Over the weekend, I described my evolving thoughts on the use of $location in a route-driven AngularJS application. And, while I think that the post was accurate from a technical standpoint, I believe it failed to capture the actual workflow of $route / $location consumption in a complex user interface (UI). As such, I wanted to do a quick follow-up that might better articulate how I think the route and the overall $location can work together to render a View.

In the previous post, I talked about how I use the route to determine which view is rendered (ie, the location of the user within the landscape of the application); and, how I use the $location to [help] define how the selected view should behave once it's rendered. To demonstrate this, I thought it would be useful to actually have a changing view. So, for this particular demo, I have two views that are mutually exclusive. Using links, the user can select one at a time - the route will determine which view is rendered and the location query string will determine the scroll-offset of the view once it is rendered.

// NOTE: While we are not referencing the $route service, we have to inject it or

// the "$routeChangeSuccess" event will never be fired (as AngularJS will only

// create the $route service on-demand).

app.controller(

"AppController",

function( $scope, $route, $routeParams ) {

// I determine which section is rendered.

$scope.section = null;

// I update the rendered section to reflect the route configuration.

$scope.$on(

"$routeChangeSuccess",

function handleRouteChangeEvent( event ) {

// The $routeChangeSuccess event will fire even if the query

// string is the only thing that changes. As such, just ignore

// any events that don't result in an actual route-change.

if ( $scope.section === $routeParams.section ) {

return;

}

$scope.section = $routeParams.section;

console.log( "Route Changed:", $scope.section );

}

);

}

);

// -------------------------------------------------- //

// -------------------------------------------------- //

// I help transition the state of the currently rendered view.

app.directive(

"bnView",

function( $location ) {

// Return the directive configuration.

return({

link: link,

restrict: "A"

});

// I bind the JavaScript events to the local scope.

function link( scope, element, attributes ) {

// The current element is the "content" which is being scrolled

// within the context of the parent. As such, we need to affect the

// scrolling on the parent element.

// --

// NOTE: It would probably be cleaner to put this directive directly

// on the parent element; but this approach keeps the demo simple.

var parentNode = element.parent()[ 0 ];

// Grab the "scroll" property out of the query string. This value

// determines how we scroll the viewport. If no value is present,

// we'll assume the viewport should scroll to the top.

var scroll = ( $location.search().scroll || "none" );

// Since the query string is only taken into account during the

// linking phase, just remove it from the URL. This way, the user

// won't be tempted to play with it.

$location.search( "scroll", null );

// If none, then scroll to the top of the viewport.

if ( scroll === "none" ) {

return( parentNode.scrollTop = 0 );

}

// If same, then keep the viewport scrolled to the same position.

// Here, we can just return because this is the natural behavior of

// the viewport (assuming some other directive didn't force a repaint

// while the content was will loading).

if ( scroll === "same" ) {

return;

}

// If we've made it this far, assume the provided scroll value is an

// integer - scroll the viewport appropriately.

parentNode.scrollTop = parseInt( scroll, 10 );

}

}

);

</script>

</body>

</html>

As you can see, while the $route and $routeParams determine which view will be included, the scroll behavior is determined by the $location search (query string) and is consumed by the linking function on the view directive. Since the scroll behavior is only observed during the linking phase, I'm removing the "scroll" query string value after it is consumed (by setting it to null).

Hopefully this demo does a bit more to clarify my thoughts on a more robust consumption of the $route and $location services in a complex AngularJS application; rather than using one or the other, both the $route and the $location services can be used to drive different aspects of the application.

I don't know too much about the ui-router. The core AngularJS router has always been sufficient for my needs. I am not sure how much cleaner it would have made this demo. That said, the part that's actually involved in the routing is the $routeChangeSuccess handler, so I am not sure how much of a win it would have been.

As far a bug, the response to the $location scroll request is in the link() function of the view. So, if you won't change the view and cause a re-linking of the directive, you won't get a change in the vertical offset. But, if you go from A to B and back, the scroll should always work (at least in my Firefox and Chrome).

If you wanted to be able to jump from one offset to another, in the same view, you could always add an event listener to the $locationChangeSuccess in the link() function. Then, it could change the offset any time the relevant URL search param changed.

I am the co-founder and lead engineer at InVision App, Inc — the world's leading prototyping,
collaboration & workflow platform. I also rock out in JavaScript and ColdFusion 24x7 and I dream about
promise resolving asynchronously.