For AngularJS applications that use the standard ngRoute facility for navigation between multiple pages, a common issue is that while the ng-view directive allows the main body of the page to vary between routes, the surrounding template is frustratingly static. Even something as simple as updating the main page title is not supported natively.

Many applications have worked around this by having the controller manipulate state -- either setting window.title directly, or writing data into $rootScope -- but that can often lead to presentational concerns bleeding into the controller.

This article explores some techniques for allowing the view's template to interact with the site's chrome, and thus allowing these concerns to be solved in a more appropriate layer.

Route-specific Title

When navigating between views in an AngularJS application, it's desirable for the HTML <title> element to be updated so that a user with multiple tabs open can quickly understand the context of each of them, or so that a browser bookmark will recieve a sensible name.

A trivial way to accomplish this is to simply update the title in the route's controller:

function controller($scope, $window) {
    $window.title = 'An Awesome Page';
    // ...other controller logic
}

However, the precise formatting of the title, much as with other aspects of how the content is presented to the user, is rightfully the concern of the template layer rather than the controller layer.

What if we could express the title as part of the view's template?

<view-title>An Awesome Page</view-title>
<h2>An Awesome Page</h2>
<div> ...and other template stuff... </div>

AngularJS directives allow us to support additional elements in templates, but this one is unusual in that it ought to have no affect on the rendering of the body of the document. Instead, we simply want to make the contents of the view element available for use in the page title.

This unusual behavior can be achieved simply by removing the element from the DOM during linking. The directive remains active in spite of its element being detached, and any interpolations within it will work as expected since watchers are attached to scopes rather than to DOM elements:

module.directive(
    'viewTitle',
    function ($rootScope) {
        return {
            restrict: 'E',
            link: function (scope, iElement, iAttrs) {

                // Remove the element from the main DOM tree so that
                // it won't be rendered.
                iElement.remove();

                // Watch the contents of the element and reflect them
                // into the 'viewTitle' variable on the root scope.
                scope.$watch(
                    function () {
                        return iElement.text();
                    },
                    function (newTitle) {
                        $rootScope.viewTitle = newTitle;
                    }
                )

                // Remove the view-specific title when the view scope
                // is unloaded.
                scope.$on(
                    '$destroy',
                    function () {
                        delete $rootScope.viewTitle;
                    }
                );

            }
        }
    }
);

Here we just reflect the text content of the view-title element into the viewTitle variable, so that we can use it within the title element in the application's main template:

<html>
  <head>
    <title ng-bind="viewTitle ? viewTitle + ' - Awesome App' : 'Awesome App'">
        Awesome App
    </title>
  </head>
  <body>
      ... etc, etc ...
  </body>
</html>

Watching the element's text content is a tricky way to automatically benefit from Angular's existing interpolation of the body, but performance-concious applications will probably want to avoid traversing DOM in a watcher by manually interpolating the text in the element at compile time.

Don't Repeat Yourself

Since the "view title" is often present inside some visible element on the page, we can avoid repetition by extending the directive to also support use as an attribute on an existing element. This is a simple matter of adding A to the restrict key in the directive definition, and then only conditionally removing the affected element:

var tagName = iElement[0].tagName.toLowerCase();
if (tagName === 'view-title' || tagName === 'viewtitle') {
    // We're being used directly as an element, so hide the element.
    iElement.remove();
}

The view title can then be "borrowed" from another element in the view template:

<h2 view-title>An Awesome Page</h2>
<div> ...and other template stuff... </div>

Since the view-title directive is originally encountered by angular inside the ng-view directive, references to variables remain relative to the view scope even though the title is presented outside of it.

This technique, along with a related technique for adding metadata elements to the document's head, is implemented in my AngularJS library angularjs-viewhead, which includes a working example.

Route-specific Sidebar

The above technique of using directives to latch on to elements and then move them around in the DOM can be extended to apply to visible elements too. Imagine for example an app with a sidebar that normally displays fixed content on each view, but for some views the sidebar switches to show some view-specific content, such as some context-sensitive related links.

Taking a cue from Django Templates and Jinja2, we can support template "blocks" that are established by the main page template but can be overridden on a view-by-view basis.

Here's a basic page skeleton with a "sidebar":

<html>
   <!-- head elided for brevity... -->
   <body>
       <div ng-view></div>
       <div class="sidebar">
           <div block-insertion="sidebar">
               <p>Default Sidebar Content</p>
           </div>
       </div>
   </body>
</html>

And then here is an example view template defining custom sidebar content:

<h1>Page With Custom Sidebar</h1>
<p>This page has a custom sidebar.</p>
<div block-replacement="sidebar">
    <p>Overridden Sidebar Content</p>
</div>

These two templates introduce two new directives: block-insertion, which marks an element as being a replacable block, and block-replacement, which can be used in a view template to replace the insertion element with the matching key.

For simplicity we'll have these directives collaborate via a global variable, but in a real implementation it would be better to use something attached to the injector so that multiple applications can coexist on one page. These two directives are also tightly coupled together for simplicity's sake.

// Insertion element for each key.
var blocks = {};

module.directive(
    'blockInsertion',
    function () {
        return {
            restrict: 'A',
            link: function (scope, iElement, iAttrs) {
                var key = iAttrs.blockInsertion;
                blocks[key] = iElement;

                // Assume that block-insertion directives won't get
                // removed after they're inserted. A more robust
                // implementation would clean these up on $destroy.
            }
        };
    }
);
module.directive(
    'blockReplacement',
    function () {
        return {
            restrict: 'A',
            link: function (scope, iElement, iAttrs) {
                var key = iAttrs.blockReplacement;

                // Prepare to move our element to its corresponding
                // block insertion point.
                iElement.remove();

                if (! blocks[key]) {
                    // No matching block insertion, so just hide.
                    return;
                }

                blocks[key].replaceWith(iElement);

                scope.$on(
                    '$destroy',
                    function () {
                        // Restore the insertion element.
                        iElement.replaceWith(blocks[key]);
                    }
                );
            }
        }
    }
);

Much as with the view-title case above, Angular's template compiler treats the replacement element as part of the view scope even though it has been moved elsewhere in the DOM, allowing variables from the view scope to be used in the replacement template.

There is a complete, working example of this technique which others are invited to build upon.

This ought to be made more robust before use in a production application. For example, it out to support the destruction of block-insertion directives, which requires retaining a little more state. It would also be nice for the block replacement to support $animate to allow transitions between blocks.

Conclusion

Enthusiasts for the alternative ui-router implementation of routing often criticize the limitations of the stock ngRoute, but with some creative use of directives and scope one can achieve some more advanced capabilities without switching away wholesale from Angular's standard routing.

The fact that directives "own" an HTML element but are managed within the scope tree allows for such elements to be moved arbitrarily around the document or removed from the document altogether while still retaining their connection to their original declaration context. This creates some powerful possibilities and I'm excited to see what other techniques can come from this capability.

All code snippets within this article are released into the public domain in the hope that they will be useful, but it is presented only as prototype code and has no warranty of any kind.