Ben Nadel
On User Experience (UX) Design, JavaScript, ColdFusion, Node.js, Life, and Love.
Ben Nadel at BFusion / BFLEX 2009 (Bloomington, Indiana) with: Dee Sadler
Ben Nadel at BFusion / BFLEX 2009 (Bloomington, Indiana) with: Dee Sadler@DeeSadler )

Providing State-Transformation Triggers Using $location In AngularJS

By Ben Nadel on

When I first started using routes, nested views, and deep linking in AngularJS, I thought that everything in the URL had to be related to the route. As such, I rarely ever used the $location service, except to change the path in order to trigger a new route-change event. Over time, however, I came to embrace the fact that the $location service was a wonderful way to provide state-transformation information for the target View after the route had changed successfully.


 
 
 

 
 
 
 
 

Run this demo in my JavaScript Demos project on GitHub.

When it comes to the URL in a single-page AngularJS application, the URL, while behind a hash (or hash-bang), mimics a normal URL. Meaning, it has a path, a search query, and a fragment (or hash) in the same way that a normal URL does. And, while the entire URL technically represents the state of your application, I have started to think about different components of the URL in different ways.

NOTE: Html5 mode blurs this line; but, I don't have any personal experience with it yet.

Right now, I think about the route - the path - as defining which View(s) will be rendered. In essence the route defines the location of the user within the landscape of the application. The query string, on the other hand, provides additional information - often optional - about the state and/or state-transformation of the rendered View.

Before I embraced this dual-purpose URL mentality, I used to jump through a lot of unnecessarily complex hoops in order to get transient state information to pass from one View to the next. This typically required me to lean very heavily on shared parent Scopes, creating tightly coupled controllers. By moving this information into the search query of the URL, it removed a lot of cruft, helped decouple the controllers, and made the code much easier to understand.

I didn't really know what kind of a demo would exemplify this philosophy; so, rather than trying to be too relevant, I just created a demo that used both the route [path] and the query string to help update the view independently. In the following code, the route defines the location of a maker while the query string defines the look and feel of the marker. I felt like this mapped decently to app-location vs. view-state.

  • <!doctype html>
  • <html ng-app="Demo">
  • <head>
  • <meta charset="utf-8" />
  •  
  • <title>
  • Providing State-Transformation Triggers Using $location In AngularJS
  • </title>
  •  
  • <link rel="stylesheet" type="text/css" href="./demo.css"></link>
  • </head>
  • <body>
  •  
  • <h1>
  • Providing State-Transformation Triggers Using $location In AngularJS
  • </h1>
  •  
  • <!--
  • In the following links, we can think of the HREF attribute as affecting two
  • different sets of data: the route and the location. Technically, the route
  • is a subset of the location; but, I tend to think about the route and the
  • location as having two different responsibilities (in a context that uses
  • routes). The route maps a location onto a particular View while the location
  • can provide additional information as to how the state of the target View should
  • change or behave after the route has changed.
  •  
  • NOTE: In a context that does not use routes, then the $location service can
  • provide everything related to the URL.
  • -->
  •  
  • <ul>
  • <li>
  • <a href="#/250/80">/250/80</a>
  • </li>
  • <li>
  • <a href="#/250/80?color=gray">/250/80?color=gray</a>
  • </li>
  • <li>
  • <a href="#/250/80?color=gold">/250/80?color=gold</a>
  • </li>
  • </ul>
  •  
  • <ul>
  • <li>
  • <a href="#/700/80">/700/80</a>
  • </li>
  • <li>
  • <a href="#/700/80?color=magenta">/700/80?color=magenta</a>
  • </li>
  • <li>
  • <a href="#/700/80?color=yellow">/700/80?color=yellow</a>
  • </li>
  • </ul>
  •  
  • <ul>
  • <li>
  • <a href="#/700/400">/700/400</a>
  • </li>
  • <li>
  • <a href="#/700/400?color=lavender">/700/400?color=lavender</a>
  • </li>
  • <li>
  • <a href="#/700/400?color=aquamarine">/700/400?color=aquamarine</a>
  • </li>
  • </ul>
  •  
  • <ul>
  • <li>
  • <a href="#/250/400">/250/400</a>
  • </li>
  • <li>
  • <a href="#/250/400?color=steelblue">/250/400?color=steelblue</a>
  • </li>
  • <li>
  • <a href="#/250/400?color=tomato#hello">/250/400?color=tomato#hello</a>
  • </li>
  • </ul>
  •  
  • <div bn-marker>
  • <br />
  • </div>
  •  
  •  
  • <!-- Load scripts. -->
  • <script type="text/javascript" src="../../vendor/angularjs/angular-1.3.8.min.js"></script>
  • <script type="text/javascript" src="../../vendor/angularjs/angular-route-1.3.8.min.js"></script>
  • <script type="text/javascript">
  •  
  • // Create an application module for our demo.
  • var app = angular.module( "Demo", [ "ngRoute" ] );
  •  
  •  
  • // -------------------------------------------------- //
  • // -------------------------------------------------- //
  •  
  •  
  • // Configure the route provider.
  • app.config(
  • function( $routeProvider ) {
  •  
  • // While the route doesn't get resolved to an action, we need it so that
  • // AngularJS knows how to map the route onto the X/Y parameters.
  • // --
  • // NOTE: The second argument is required, but we don't use it.
  • $routeProvider.when( "/:x/:y", {} );
  •  
  • // If the user hits a route that can't be mapped, just redirect to a
  • // known, valid route configuration.
  • // --
  • // NOTE: This will redirect the initial page request if no route is
  • // present on page load.
  • $routeProvider.otherwise( "/500/300" );
  •  
  • }
  • );
  •  
  •  
  • // -------------------------------------------------- //
  • // -------------------------------------------------- //
  •  
  •  
  • // I manage the marker, keeping it synchronized with the state of the route and
  • // the location services.
  • app.directive(
  • "bnMarker",
  • function( $location, $route, $routeParams ) {
  •  
  • // Return the directive configuration object.
  • return({
  • link: link,
  • restrict: "A"
  • });
  •  
  •  
  • // I link the JavaScript events to the local scope.
  • function link( scope, element, attributes ) {
  •  
  • // These are the marker properties that will be translated into CSS
  • // and content properties.
  • var x = -300;
  • var y = -300;
  • var color = "snow";
  • var text = "";
  •  
  • // Set up the initial CSS asynchronously. This way, we make sure not
  • // to force a repaint while the views are still linking.
  • scope.$evalAsync(
  • function setupCss() {
  •  
  • element
  • .addClass( "marker" )
  • .css({
  • left: ( x + "px" ),
  • top: ( y + "px" )
  • })
  • ;
  •  
  • }
  • );
  •  
  • // When the location changes, we want to look at the search query to
  • // see if there are any relevant changes. We'll defer the X/Y
  • // coordinates to the route-change event once the location is parsed
  • // into relevant parameters.
  • // --
  • // NOTE: Location change events take into account all aspects of the
  • // URL, including the path, the query, and the fragment.
  • scope.$on(
  • "$locationChangeSuccess",
  • function handleLocationChangeEvent( event ) {
  •  
  • console.log( "Location Change:", $location.search() );
  •  
  • // Apply the color, if it has changed.
  • if (
  • $location.search().color &&
  • ( $location.search().color !== color )
  • ) {
  •  
  • element.css({
  • backgroundColor: ( color = $location.search().color )
  • });
  •  
  • }
  •  
  • // Apply the text, if it has changed.
  • if (
  • $location.hash() &&
  • ( $location.hash() !== text )
  • ) {
  •  
  • element.text( text = $location.hash() );
  •  
  • }
  •  
  • }
  • );
  •  
  • // The location change has been parsed and successfully resolved into
  • // a route with route mapped parameters.
  • // --
  • // NOTE: While the $routeParams will contain the $location.search()
  • // values as well as the path-based parameters, this event only
  • // gets called when the location change can be mapped onto a route.
  • // As such, it's probably better to defer all non-route data to the
  • // $locationChangeSuccess event in order to make sure that it doesn't
  • // get missed.
  • scope.$on(
  • "$routeChangeSuccess",
  • function handleRouteChangeEvent( event ) {
  •  
  • console.log( "Route Change:", $routeParams );
  •  
  • element.css({
  • left: ( ( x = $routeParams.x ) + "px" ),
  • top: ( ( y = $routeParams.y ) + "px" )
  • });
  •  
  • }
  • );
  •  
  • }
  •  
  • }
  • );
  •  
  • </script>
  •  
  • </body>
  • </html>

As you can see, when the $location changes, we can respond to the $locationChangeSuccess event. And, when the route changes, we can respond to the $routeChangeSuccess event. Keep in mind that there will be a lot of cross-over in these two events; since the route is driven by a subset of the URL components, every route change will also result in a location change. The reverse, however, is not always true - we can have a location change that does not result in a route change.

In AngularJS, there's so much emphasis on storing information in services and controllers, it's easy to forget that the URL can contain valuable information about the state of the page. Both the route and the query string can work together to determine not only which View to render but, how that view should behave once it is rendered.




Reader Comments

Post A Comment

You — Get Out Of My Dreams, Get Into My Comments
Live in the Now
Oops!
Comment Etiquette: Please do not post spam. Please keep the comments on-topic. Please do not post unrelated questions or large chunks of code. And, above all, please be nice to each other - we're trying to have a good conversation here.