Passing State via Routes
AngularJS - Communicating Between Controllers - Part 3
In the previous two articles, we discussed passing state using scope variables and Angular's PubSub mechanism.
Another, often overlooked mechanism, is the URL.
I'm going to assume you already understand the concept of URL routing in an client side application. If you don't, I suggest you do some precursor research on the web (explaining the concept is beyond the scope of this post).
Angular has it's own routing component, but it's pretty limited. Instead, most self-respecting Angular developers use a popular third-party library called Angular UI Router. Regardless of what framework you use, the concepts are generally the same: state will be passed in the URL as path parameters. In this post, I will demonstrate Angular UI Router.
Angular UI Router
The UI Router has the concept of a main viewport. The viewport is a location in the HTML DOM where content can be inserted depending on the current route (or state in UI Router's terms). We specify the viewport by using the Angular UI directive ui-view
, which can be either an attribute or element:
<body>
<div ui-view></div>
</body>
Keep in mind that this element should be empty; if it's not, the content will be replaced during a state transition.
State destinations (i.e. paths defined by route expressions) are specified using the UI Router's $stateProvider
:
$stateProvider.state("sprocket", {
templateUrl: "sprocket.template.html",
controller: "SprocketCtrl",
// ":id" is a path parameter.
url: "/sprocket/:id"
});
Controllers can easily get access to the path parameters by requesting the $stateParams
service:
app.controller("SprocketCtrl",
[ '$scope', '$http', '$stateParams',
function($scope, $http, $stateParams){
// Make an AJAX call, retrieving the state.
$scope.sprocketInfo =
$http.get("/api/sprocket/" + $stateParams.id)
.then(function(res){ return res.data; });
}]);
To navigate to a specific state/route, you have a couple of options.
Change the URL manually.
<a href="#/sprocket/123">Sprocket 123</a>
Or if you need to dynamically specify the route:
<a ng-href="#/sprocket/{{id}}">{{name}}</a>
Programmatically by the Angular $location
service.
$location.url("#/sprocket/" + id);
UI Router's $state
service.
$state.transitionTo("sprocket", { id: "123" });
UI Router's ui-sref
directive.
<a ui-sref="sprocket({ id: '123'})">Sprocket 123</a>
Or use variables on your scope:
<li ng-repeat="sprocket in sprockets">
<a ui-sref="sprocket({ id: sprocket.id })">{{ sprocket.name }}</a>
</li>
The UI Router supplied mechanisms, in my opinion, are the best options. These mechanisms decouple the path
from the desired state
that you want to transition to. UI Router also provides a form of inheritance with states, which makes it even easier to navigate when using their directives or API.
Cleaning Up Our Controllers
My favorite feature of Angular UI Router is the resolve
mechanism in the $stateProvider
. In almost all of our controllers, we find ourselves using the path parameters supplied by the $stateParams
service to make some kind of AJAX call to get the essential state for the view from the server. This means that our controllers are not only responsible for interactions with the view, but they are also responsible for acquisition of the initial state to render the view.
In general, this is ok; it's a kind of a traditional responsibility that controllers have had in the past with a number of frameworks. It does, however, couple us to the specific services we use to acquire data (in addition to making it more difficult to test). In our example above, we would have to inject a mock for the $http
service, and configure its behavior, in order to test interactions between the view and controller.
A simpler solution is to inject the initial state into the controller, instead of having it fetched by the controller. This is what UI Router's resolve
mechanism does. It allows us to resolve
variables before a controller is activated during a state transition. Better yet, if any variable being resolved returns a promise
, UI Router will wait until the promise is resolved before activating the controller. This means, no temporary waits when you transition as your controller loads it's state.
Taking the previous example, I'll rewrite the state declaration and the controller to demonstrate resolve
's power:
app.config(
["$stateProvider", function($stateProvider){
$stateProvider.state("sprocket", {
templateUrl: "sprocket.template.html",
controller: "SprocketCtrl",
url: "/sprocket/:id",
// Here we will resolve variables.
resolve: {
// Let's fetch the sprocket in question
// so we can provide it directly to the controller.
sprocket: function($http, $stateParams){
var url = "/api/sprocket/" + $stateParams.id;
return $http.get(url)
.then(function(res){ return res.data; });
}
}
});
}]);
app.controller("SprocketCtrl",
[ '$scope', 'sprocket', function($scope, sprocket){
$scope.sprocketInfo = sprocket;
}]);
Look at how much cleaner the controller is! Now we can test simply by injecting our own sprocket instance (instead of a mock of $http and $stateParams).
Benefits and Pitfalls of Router Communication
Benefits
- Cleanly models natural transitions in your application.
- Completely decouples controllers while providing a way to signal each other to activate.
- Allows preloading of state prior to a transition (benefit of UI Router framework).
Pitfalls
- Only a limited amount of state can be transferred (as URL params).
- Communication only occurs during the transition. In many cases, the communication is the last act of the sender before it is destroyed.
Code
I have a fully functional example of using UI Router to pass state between controllers. This example is not the same as the code excerpts posted in this article (the real example is much more complex and I wanted to keep the topic short). You can review the code on Github: https://github.com/rclayton/NG-Communicate-Ctrls/tree/master/router
Stumbling my way through the great wastelands of enterprise software development.