Track pending web requests with AngularJS

Navigation Guard

In the previous two posts (here and here), I covered the AngularJS factory, saNavigationGuard that I had created. This factory provides a simple way to guard against inconvenient page navigation in our Angular applications. However, up until now, I have not demonstrated one of the coolest uses for this. Before we look at this awesome use of saNavigationGuard, let us take a short trip into late last year.

I was getting started on my first big project with AngularJS and I had added a busy indicator to my user experience. To control when the indicator was active, I used a counter. This counter was incremented and decremented before and after using Angular resources. It was a nest of promises and error handlers, just to make sure the counter worked properly. I was not entirely happy with it but it was the best I could work out from my knowledge of Angular and JavaScript, so I submitted a pull request. My colleague reviewed the work and, looking at the busy indicator and the code to control the counter, stated, "there must be a better way."

I did not know at the time, but he was right, there is a better way and it uses an Angular feature called "interceptors".

Interceptors

Interceptors provide hooks into web requests, responses and their errors allowing us to modify or handle them in different ways. They are provided via an AngularJS provider such as a factory or service and injected into the $httpProvider using config as follows.

angular.module('somewhatabstract').config(['$httpProvider', function($httpProvider) {
	$httpProvider.interceptors.push('saHttpActivityInterceptor');
}]);

In this snippet, the name of the interceptor, saHttpActivityInterceptor, is added to the array of interceptors on $httpProvider.

The interceptor itself is a little more complex.

angular.module('somewhatabstract').factory('saHttpActivityInterceptor', ['$q', 'saNavigationGuard', function($q, saNavigationGuard) {
	var pendingRequestsCounter = 0;

	var updateCounter = function(method, delta) {
		if (method != 'POST' && method != 'PUT' && method != 'DELETE') {
		    return false;
		}
		pendingRequestsCounter += delta;
		return true;
	};

	saNavigationGuard.registerGuardian(function() {
		return pendingRequestsCounter > 0 ? 'There are changes pending.' : undefined;
	});

	return {
		request: function(request) {
			request.saTracked = updateCounter(request.method, 1);
			return request;
		},

		response: function(response) {
			if (response.config && response.config.saTracked) {
				updateCounter(response.config.method, -1);
			}
			return response;
		},

		responseError: function(rejection) {
			if (rejection.config && rejection.config.saTracked) {
				updateCounter(response.config.method, -1);
			}
			return $q.reject(rejection);
		}
	};
}]);

The interceptor factory returns an object that, in this case, has three methods: request, response and responseError. A fourth method, requestError, can also be included in interceptors if needed. Before returning our interceptor, the interceptor factory registers a guardian with saNavigationGuard that will guard against navigation if the pendingRequestsCounter is greater than zero.

The interceptor monitors requests and responses. On each request, the request method is checked, if it is POST, PUT or DELETE, the pendingRequestsCounter variable is incremented by one and the request is tagged to indicate it is being tracked. We flag it so that we know to pay attention when the corresponding response or response error occurs. In the response and response error handlers, we decrement our counter based on the tracking flag and the method.

Finally…

The outcome of using this interceptor is that if the user tries to navigate away from the page after a request has been made but before its response has been received, they will see a message asking them to consider postponing the navigation.

In the next post, we will look at testing both this interceptor and the saNavigationGuard functionality using my preferred combination of jasmine, CoffeeScript and jasmine-given.

As always, please consider leaving a comment if you have found this post useful or have any alternatives.

Navigation guard using AngularJS: $locationChangeStart

Where were we?

In the last post, I introduced saNavigationGuard, an AngularJS factory that provided a mechanism for different components to guard against the user navigating away from the page when it was not desirable. This used the $window.onbeforeunload event and therefore, only works when navigating outside of the AngularJS application. For in-app navigation, we need to monitor a different event that Angular provides called $locationChangeStart.

$locationChangeStart

In the following code I have expanded saNavigationGuard to include support for $locationChangeStart.

angular.module('somewhatabstract').factory('saNavigationGuard', ['$window', '$rootScope', function($window, $rootScope) {
	var guardians = [];

	var onBeforeUnloadHandler = function($event) {
		var message = getGuardMessage();
		if (message) {
			($event || $window.event).returnValue = message;
			return message;
		} else {
			return undefined;
		}
	};

	var locationChangeStartHandler = function($event) {
		var message = getGuardMessage();
		if (message && !$window.confirm(message))
		{
			if ($event.stopPropagation) $event.stopPropagation();
			if ($event.preventDefault) $event.preventDefault();
			$event.cancelBubble = true;
			$event.returnValue = false;
		}
	};

	var getGuardMessage = function () {
	    var message = undefined;
	    _.any(guardians, function(guardian) { return !!(message = guardian()); });
	    return message;
	};

	var registerGuardian = function(guardianCallback) {
		guardians.unshift(guardianCallback);
		return function() {
			var index = guardians.indexOf(guardianCallback);
			if (index >= 0) {
				guardians.splice(index, 1);
			}
		};
	};

	if ($window.addEventListener) {
		$window.addEventListener('beforeunload', onBeforeUnloadHandler);
	} else {
		$window.onbeforeunload = onBeforeUnloadHandler;
	}

	$rootScope.on('$locationChangeStart', locationChangeStartHandler);

	return {
		registerGuardian: registerGuardian
	};
}]);

In this new version of saNavigationGuard, the getGuardMessage method has been introduced that is responsible for querying the guardians for a reason not to allow navigation. In addition, an event handler has been added on $rootScope to handle $locationChangeStart. This handler is then used to query the guardians and display a confirmation message.

To display the confirmation message, I settled on using $window.confirm. Originally, I had thought about allowing a custom experience instead, but I felt that, due to any alternative experience being non-modal, the state management to allow a navigation to continue would be overly complicated.

Finally…

I was expecting the modifications to support $locationChangeStart to be more difficult than it turned out, though there are certainly some improvements that can be made1.

In the next post, I am going to show how we can track pending web requests and prevent navigation accordingly. In the meantime, please comment and let me know if you have other uses for saNavigationGuard or even alternative implementations.

  1. For example, the old and new URLs that are given as arguments to the $locationChangeStart handler could be passed to the guardians in order for them to make more informed decisions. []

Navigation guard using AngularJS: onbeforeunload

onbeforeunload

I have been working on an add-on for one of our products that includes some data entry components. As part of this, I wanted to make sure that any changes to the data were saved before the user lost interest and went searching the Internet for cat pictures. I quickly turned to the window.onbeforeunload event.

The onbeforeunload  event is the last chance for a website to get the user's attention before they have navigated away to something more shiny or sparkly. In AngularJS, since a real navigation is not going to occur in a single-page application, two events are provided, $locationChangeStart  and $locationChangeSuccess, that can be used to similarly intercept in-app navigations.

Through onbeforeunload and the subsequent browser-based dialog, or the AngularJS events and a subsequent app-based dialog, the user is given the opportunity to cancel the navigation and address something important like unsaved data or a cute cat photo that they almost missed.

So, with events available to intercept navigation, I started hooking them up in all the places that needed them. A directive here, a factory there; before long I was duplicating almost the same code in nearly 20 places. There had to be a better way, so I created the saNavigationGuard service1.

saNavigationGuard

The saNavigationGuard service provides consumers with the ability to register callbacks, known as guardians, that will be called if a navigation event is detected. If a guardian wishes to prevent that navigation, it returns a message explaining why. The saNavigationGuard then signs up to the relevant events and when they occur, asks each guardian if it should go ahead. Whichever guardian returns a string first, wins, and the guardian acts accordingly to prevent the navigation.

In the case of onbeforeunload , the browser continues the event handling by offering the user a choice to continue or cancel the navigation. In the case of the $locationChangeStart  event, this part must be handled by the application. We will come back to how in another post; for right now, let's just look at the onbeforeunload scenario.

Here is the saNavigationGuard factory declaration:

angular.module('somewhatabstract').factory('saNavigationGuard', ['$window', function($window) {
	var guardians = [];

	var onBeforeUnloadHandler = function(event) {
		var message;
		if (_.any(guardians, function(guardian) { return !!(message = guardian()); })) {
			(event || $window.event).returnValue = message;
			return message;
		} else {
			return undefined;
		}
	}

	var registerGuardian = function(guardianCallback) {
		guardians.unshift(guardianCallback);
		return function() {
			var index = guardians.indexOf(guardianCallback);
			if (index >= 0) {
				guardians.splice(index, 1);
			}
		};
	};

	if ($window.addEventListener) {
		$window.addEventListener('beforeunload', onBeforeUnloadHandler);
	} else {
		$window.onbeforeunload = onBeforeUnloadHandler;
	}

	return {
		registerGuardian: registerGuardian
	};
}]);

First thing to note is on line 6 where I have used the any method from underscore (or lodash). This calls a predicate for each element in an array until the predicate returns true. In this case, we are passing our array of guardians and the predicate is calling that guardian to see if it currently wants to stop navigation. If it does, it will return a message.

If one of the guardians returns a message, the message it returns is captured and the predicate returns true. The message is then passed to the event so that the browser will show its dialog. If no guardian returns a message, then the browser is allowed to continue with the navigation unhindered (at least by this code).

Because of how factories are instantiated in Angular, the initialization code that actually signs-up to the onbeforeunload event only occurs once per Angular application. So, injecting this factory into your directives, factories, etc. means the event handling will be initialized just once. This gives a central point to control interception of navigation while allowing individual components to prevent navigation for their own reasons.

Usage

Here is a simple example of how this might be used:

angular.module('somewhatabstract').controller('saEditorThingyCtrl', ['$scope', 'saNavigationGuard', function($scope, saNavigationGuard) {
    var editing = false;
    var navigationGuardian = function() {
        return editing ? "Edit in progress" : undefined;
    };
    
    saNavigationGuard.registerGuardian(navigationGuardian);

    $scope.startEdit = function(){
        if (editing) return;
        editing = true;
        // Stuff happens
    };
    $scope.endEdit = function() {
        if (!editing) return;
        editing = false; 
    };
}]);

Of course, this is a contrived example but it illustrates how easy it is to put the saNavigationGuard to work. In fact, if you didn't want your guardian to be called all the time because you knew it usually would not block navigation, you could only have it registered when needed.

    ...
    var unregisterGuardian;
    
    $scope.startEdit = function(){
        ...
        unregisterGuardian = saNavigationGuard.registerGuardian(navigationGuardian);
        ...
    };
    $scope.endEdit = function() {
        ...
        unregisterGuardian();
        unregisterGuardian = undefined;
        ...
    };
    ...

This ability to register and unregister guardians brings me to my second noteworthy aspect of saNavigationGuard. When registering a new guardian, it is added to the start of the array of guardians rather than the end. This is because, in my use case, the more recently registered guardians are often the most urgent. I suspect this may be common for most uses.

Finally…

This post is the first in a series covering some useful things I have created in my experiences with AngularJS. In the posts to follow, I will be covering how we can expand saNavigationGuard to cover in-app navigation and how we can intercept web requests to provide both navigation guards and busy indication.

I hope you find the saNavigationGuard factory as useful as I have on my current project where I have guardians for active edits and when there are pending POST, PUT or DELETE requests.

If you use this or anything else that I have written about in your project or want to suggest some tweaks or alternatives, please don't forget to leave a comment.

  1. It's really a factory in the AngularJS world, but it's providing a service…oh, I'm not getting into this discussion again, just look up service vs. factory and read about why it doesn't matter as long as you choose. []

The Connected Vehicle

At the end of last month I attended the Automotive Megatrends 2012 held at The Henry in Dearborn, MI. Though this was a three-day event, I attended the second day only: Connectivity. It was an opportunity for major and minor players in the automotive world to present and discuss their particular visions of the future for passenger cars in a world that is increasingly connected. Particular attention was paid to the Cloud and the continuing trend for infotainment1 to be provided via handheld devices rather than proprietary in-vehicle systems. Safety was a hot topic; in particular driver distraction, where legislation tends to hold vehicle manufacturers liable in the event of an accident even though they may have little or no control over the devices that do the distracting (such as smartphones).

The day was split into four main sessions divided by networking opportunities. Each main session took the form of a panel where four or five panelists would present their views on a particular topic with a moderator overseeing the discussion. Each panel would face a round of questions once all had presented. The topic of the first two sessions was "Connected vehicle outlook — the next 10 years" with the following sessions being "Mobile device integration" and "Software and apps" respectively. Repeatedly during the day, speakers would return to the concept of the Connected Vehicle and what that means for consumers and manufacturers alike, but what do they mean by "The Connected Vehicle"?

A Day in the Life

You wake up on a cold, wintry morning to your smartphone alarm obnoxiously wailing. Via the magic of the Internet, the home management app has checked the local weather and adjusted your home heating to give you an extra bit of toasty warmth. It has also instructed your coffee machine to brew up some Joe.

You flip to the appropriate smartphone screen and start your car. A quick swipe and the in-car temperature is set just right. An alert tells you a service is due and shows you local service locations along with their cost. You select your favourite location and choose an appointment time, then you swap over to your home management app and start the shower. By the time you're out of bed, showered, dressed and have your coffee in hand, the car is thawed out and toasty warm.

As you drive to work by way of your children's daycare, information is delivered to you via your smartphone to your in-car video and audio systems, telling you the weather, headlines, social media updates and to-do list for the day. Your favourite music plays in the background as you choose. Perhaps you even queued up some things from the night before. Voice commands and a simple, radio-like interface give you simple, non-distracting control of your information streams. Everything coordinates and cooperates to ensure that you can concentrate on driving.

As you're finishing off a quick check of your e-mail subject lines an alert flashes up warning you of road construction and traffic delays. The satellite navigation app on your smartphone kicks in, offering alternative routes and travel times to get you on your way. As you begin your detour, the directional microphones and image processing systems in the back seat detect that your kid just woke up and has started punching his sibling. In an attempt to keep the peace, the latest, greatest animated movie immediately starts streaming from Netflix, Hulu or Zune in the headrest display. Meanwhile, your satellite navigation is suggesting spots to safely pull over (as well as one or two doughnut shops you might need for the purchase of "behave yourself" bribes).

Having dropped the kids off at daycare, you pull up at work and apply the parking brake. The in-car systems take the opportunity to remind you of your service appointment. You get out of the car and walk to your office – the car automatically turns off and locks itself as you go. When you get to your desk, you computer has already synced with the Cloud, showing your service appointment on your calendar along with a snapshot of your car diagnostics, should you need to discuss the appointment over the phone.

Reality Check

Though embellished with a few ideas of my own, this scenario is similar to many involving the connected vehicle envisaged by those presenting at the conference. It is all so seductively plausible that it's easy to ignore the reality.  Behind all the enthusiastic rhetoric there are so many unresolved problems and challenges that we're just not ready yet to deliver the dream of the connected vehicle. To get an idea of where we are right now, consider the current vehicle to be akin to video-game consoles just over 10 years ago. Before the current generation of consoles (Playstation 3, XBOX 360, Nintendo Wii), pretty much all you could do with a gaming console was play games, now we can not only play games, but also buy games, rent, buy and stream video, listen to Internet radio stations, watch live television (in HD) and interact with social networks.

The problems for the connected vehicle mostly lie in the gap between the old and the new; passenger cars, with a development cycle of 3-4 years and consumer electronics, with a development cycle of 12-18 months. In a world where a smartphone can be out-of-date within a year but a car is expected to last ten or more, bridging the gap becomes a challenge. Not to mention that the world of the connected car relies on the existence of wireless carriers and services that not only support the demands of consumers but also those of the equipment manufacturers, services like OnStar and its soon to be released API, requiring access to vehicle data and systems in a safe and secure manner.

Controlled Openness

To bridge the development cycle gap, there was a call for the end of proprietary infotainment systems and more controlled, open standards across the passenger car industry. The general view was that proprietary systems have to go in favour of smartphone or other smart device apps, a trend that has already begun. This move would help to reign in the growing concerns surrounding driver distraction by providing an in-vehicle delivery platform that allows apps to interact with the car and its passengers in a safe, secure and reliable manner.

In order to make such a platform appealing to app developers, a set of open standards needs to be adopted by the industry, a set of standards that has not yet been defined but that will provide rules and guidance on how an app interacts with a vehicle and its occupants (as with any new technology discussion of 2012, whispers of HTML5 were everywhere). This idea of controlling app delivery within the vehicle while allowing open standards and app development was dubbed "controlled openness" and clear comparisons were drawn with Apple and the way they govern the app marketplace.

Safe and Secure

Just like the API provided by Apple and any other contemporary development platform, security is extremely important. Security is the basis of trust for consumers and without it the full potential of a technology can never be realised as no one will ever immerse themselves fully. Several presenters gave their thoughts on how security might work but there was a lack of convincing argument that this was a simple problem to solve. In fact most speakers on the matter seemed to be plugging a product while skirting around some of the issues that had been raised by others. Issues that have names like "virus", "hacker" and "theft"; the connected vehicle opens up a cornucopia of problems that must be resolved.

  • How do you stop someone taking control of your vehicle while allowing you to remote start it from your phone?
  • How do you allow an app access to vehicle systems without allowing a bug to cause a vehicle accident?
  • How do you ensure that a person's identification is unpaired from a vehicle when they are no longer in possession of that vehicle due to sale, accident or theft?

Given the need to exchange data to and from the vehicle communications network in order to support telematics and other advanced (perhaps premium) apps, which may include the ability to do things like start, stop or even track the vehicle, I'm sure you can think of many other scenarios that highlight how important it is that the connected vehicle be secure.

The Internet and our increasingly connected world has security all over the place with a plethora of approaches to providing identification, authorization and secure access. However, the effects of a hack or security flaw have so far not had such potentially immediate fatal results as they might in the world of the connected vehicle. A security breach that allows someone to take control of some aspect of your car is entirely unacceptable. This is not a case of making sure it should never happen, but rather a case of could never happen. If nothing else, the experience of driving a car must be safe, both actually and perceptually.

The Road Ahead

So where does that leave us? The automotive industry has rightly identified a need to integrate more closely with the consumer electronics world and move away from the proprietary in-car infotainment systems of old, but the consumer electronics industry is racing along at quite a pace. Although the concept of a smartphone existed prior to its announcement, the launch of the iPhone five years ago accelerated smartphone evolution and it shows no signs of slowing down.  However, until the iPhone of the connected vehicle concept appears and focuses consumer expectations, we will have to accept the Windows Mobile-style missteps along the way2.

While the connected vehicle is still an uncertain concept, it is becoming a reality and it will change the way we interact with our cars. In fact, they may not be our cars at all3. The speakers at the Automotive Megatrends 2012 event had plenty of statistics, ideas and products to illuminate the target that is the connected vehicle. Now all we need to do is find the road that takes us there.

  1. Infotainment is a word used in the automotive industry to refer to the combined provision of information and entertainment services within a vehicle such as radio and satellite navigation []
  2. Not to be confused with Windows Phone 7 (or 7.5), which is awesome. []
  3. Uncertainty exists on how various facets of the connected vehicle will be monetized; from the services and apps to the car itself. Will it be subscription-based, ad-supported or freemium? Will we buy our cars or enter into a service-agreement instead? All of these things and more are yet to be determined. []