Using AngularJS ui-router Query Parameters and Complex Directives Together Without Killing App Performance

This is a presentation that I gave at the Boise AngularJS meetup.  The Plunker that goes along with this can be followed along with here.

The Problem

You have an application where you want to allow the user to pan an image.  This is a large image which you cannot display on the screen at one time.  In this example, I'm using the AngularJS shield logo, but a more pertinent example might be a geo-mapping application, or a data visualization application.  In an application like this, you may prefetch more data than is actually shown on the screen to be rendered quicker as the user performs a few fairly predictable actions (like moving it around).  Think of an application like Google Maps.  If you are looking at your city, you most likely are going to drag it around a little bit, because you might be travelling.  You might pre-fetch neighboring map images so that the drag feature would be able to be fast since you can almost predict it happening.

The other requirement is that the location within the image that the user pans to must be captured in the URL to allow permalinking to a particular view.  We want the user to be able to just copy and paste, or socially share, the URL in the address bar with friends.  This is also a lot like Google Maps, you can find your favorite barcade and email it to your buddies by clicking a share button or copy and pasting the URL.

With that requirement, your URLs will then look something like: /image?x=50&y=100

Where x and y represent the offsets into the image.  With a map, these would probably be latitude and longitude.

You initially implement this within ui-router as a single state with query args, like:

.config(function config($stateProvider) {
  $stateProvider.state('single', {
    url: '/single?x&y',
    controller: 'SingleCtrl',
    templateUrl: 'single.html'
  })
})

See the Plunker in the single.js file for this code.  Then in the Angular way, you decide to encapsulate the image loading and panning in a directive within single.html.  Like this:

<canvas my-image image="imageurl" x="navigation.x" y="navigation.y" width="400" height="400"></canvas>


You design your directive to be attached to a canvas element as an attribute, with directive attributes for the x and y position. Within the directive, you then load the image into memory which requires a fetch from the server, then write it to the canvas element within this directive.

When you start panning around, you notice that it is not very responsive.  There is a delay every time you switch the x and y coordinate.  You can see this within the Plunker when looking at the Single tab.  Use the Pan buttons to move the image around.  (For this exercise, I've added in a 500ms delay to simulate a delay in processing in a complex directive).

This is due to the fact that with ui-router, every time a query arg is changed, it is a state change, the controller is executed again, and the template is re-rendered.

The Solution

Follow the pattern of splitting your states with query arguments into two states, one for the "base" state, and a sub-state that handles the query arguments.

Your URLs will then look something like /image/view?x=50&y=100 as opposed to /image?x=50&y=100

The "base" state url is /image and the child state for the query parameters are /view?x&y.  Like:

.config(function config($stateProvider) {
  $stateProvider.state('double', {
    abstract: true,
    url: '/double',
    controller: 'DoubleCtrl',
    templateUrl: 'double.html'
  });
 
  $stateProvider.state('double.view', {
    url: '/view?x&y',
    controller: 'DoubleViewCtrl',
    template: '<div></div>'
  });  
})

You can see this in the Plunker in the double.js file.  Note: You cannot just have a child state of ?x&y, it does not work, must have a pattern to match on before the ?.

This allows the "base" state controller and template (which contains the directive) to stay as an active state and not be re-executed and re-rendered when just the URL query parameters change.

The fundamental difference between this two state and the single state example is that the query parameter handling code is pushed down into the child state, and changes the x and y inherited from the base state's scope, thus causing the directive to change.  Notice in the Plunker how the logic that was in the SingleCtrl is now split between DoubleCtrl and DoubleViewCtrl.  This actually leads to a very nice natural split in the functionality of the code, feels like better design.  All the query parameter handling logic to map back to an inherited parent navigation object that contains those same query parameters is in the DoubleViewCtrl which is the state with the query parameters in the URL.  Makes sense, right!

This "base" state can also most correctly be declared as abstract: true which means it cannot be transitioned to directly.

Now that the directive does not get re-created every time a query parameter changes.  It only changes the child state, not the base state.  This means you can just move the image around and copy to canvas very efficiently because it remains in memory.  Notice this by going onto the Double tab within the Plunker, you can see the execution count there, the parent state "double" controller and directive inside it's template only get called once.  Only the child state "double.view" controller gets executed with each click on the buttons.