Using AngularJS decorators to resolve view templates by resource content type
It is quite common in our applications to retrieve a resource via a link, and then find a view to render that resource, based on its content type.
For example, we might have resources like this:
{ contentType: 'application/vnd.endjin.test.greeting', content: { greeting: 'hello', greet: 'world' } }
{ contentType: 'application/vnd.endjin.test.salutation', content: { salutation: 'hail', greet: 'caesar' } }
And one view that binds to that first resource type
<div ng-controller="GreetingController">
<p><span ng-bind="content.greeting"></span> <span ng-bind="content.greet"></span></p>
</div>
And another that binds to that second resource type
<div ng-controller="SalutationController">
<p><span ng-bind="content.salutation"></span> <span ng-bind="content.greet"></span></p>
</div>
Notice that the views have controllers specific to the content type of the resource they expect to present, and bind to properties of the content of that type.
Let's assume that there's a parent controller that gets this resource from some url that may return one of a number of different 'greeting-like' resources of this kind (think of it as a polymorphic type in classical OO terms). This then presents the appropriate view by doing an ng-include of the relevant view, using the content type.
<div class="someParent">
<div ng-include="resource.contentType"></div>
</div>
If you try this at home, and look at your network traffic, you'll see that the ng-include directive uses the $templateCache to go off and try to find the view. Unfortunately, this makes an HTTP request for a resource at "/application/vnd.endjin.test.greeting".
While you could construct a controller which returns an appropriate resource at that path, what we'd really like to do is to translate this into a request for an appropriate view for the content type.
One way to do this would be to provide a mechanism on our parent controller to do this translation:
$scope.translateContentTypeToUri = function(contentType) {
return '/api/views/' + encodeURIComponent(contentType);
};
And then call that from our view
<div class="someParent">
<div ng-include="translateContentTypeToUri(resource.contentType)"></div>
</div>
Of course, this would require changing all our controllers (even if we wrapped the actual work up into a service they all consume).
And the code in the view is pretty ugly, too.
A better approach might be to create a decorator for the $templateCache to do the lookup for us, behind the scenes.
Here's an example using that technique:
app.config(["$provide", function ($provide) {
$provide.decorator("$templateCache", ["$delegate", "templateRepository", function ($delegate, templateRepository) {
// Stash away the original get method
var origGetMethod = $delegate.get;
// Replace it with our getter
$delegate.get = function (url) {
// Do we match our content type family prefix?
var prefix = 'application/vnd.endjin.test';
if (url.slice(0, prefix.length) == prefix) {
return templateRepository.getTemplate(url);
}
// Otherwise, use the original get method
return origGetMethod(url);
};
return $delegate;
}]);
}]);
The very cool thing about this is that I can have multiple view providers (e.g. for different families of content types), and each can register its own decorator, and they will all get a go!
The templateRepository service that this code depends on abstracts the lookup of the template content type (so it could be as simple as a loop back through the $templateCache for a specific named template).
Obviously, you could make this more complex - defer a lookup over HTTP using promises, for example, but the principle is the same.
And our view goes back to being very clean and simple:
<div class="someParent">
<div ng-include="resource.contentType"></div>
</div>