AngularJS: Managing active nav elements

The header and footer of an application are typically outside the scope of the content or controller you're working on. This makes managing the navigation state a little more challenging. In my case, managing navigation state is simply applying an active class to a particular element.

Take the markup below for example. ng-view is the area my content controller is generating content. The problem is that when I invoke #/view1, the primary navigation is outside the scope of the view1 controller.

Note, I'm using the angular-seed project as the base of my example in case you would like to reference the structure I'm basing this on.

index.html

<html lang="en" ng-app="myApp">
<body>
  <ul class="menu">
    <li><a href="#/view1">view1</a></li>
    <li><a href="#/view2">view2</a></li>
  </ul>
  <div ng-view></div>
  <script src="//ajax.googleapis.com/ajax/libs/angularjs/1.0.7/angular.min.js"></script>
</body>
</html>

A simple way to approach this is to wrap your navigation in its own controller, and use the $location service to detect the path.

In the code below, I added the NavCtrl

controller and injected $scope and $location. This allows us to access $location, and more specifically, $location.path, in the markup.

js/controllers.js

'use strict';
angular.module('myApp.controllers', []).
  controller('NavCtrl', ['$scope', '$location', function($scope, $location) {
    $scope.$location = $location;
  }])
  .controller('MyCtrl1', [function() {
  }])
  .controller('MyCtrl2', [function() {
  }]);

Once the controller is in place, use the ng-controller directive to apply the controller to the navigation. Then we can use the ng-class directive to conditionally set the class based on $location.path.

The markup now looks like this:

index.html

<html lang="en" ng-app="myApp">
<body>
  <ul ng-controller="NavCtrl" class="menu">
    <li ng-class="{active: $location.path() == '/view1'}"><a href="#/view1">view1</a></li>
    <li ng-class="{active: $location.path() == '/view2'}"><a href="#/view2">view2</a></li>
  </ul>
  <div ng-view></div>
  <script src="//ajax.googleapis.com/ajax/libs/angularjs/1.0.7/angular.min.js"></script>
</body>
</html>
That is all you need to do, but continue to see one way of cleaning it up a little more.

You can take it a step further by managing the navigation array inside the controller, and create a function that tests if the item is equal to the current path.

js/controllers.js

'use strict';
angular.module('myApp.controllers', []).
  controller('NavCtrl', ['$scope', '$location', function($scope, $location) {
    $scope.items = [
      {path: '/view1', title: 'View 1'},
      {path: '/view2', title: 'View 2'},
    ];
    $scope.isActive = function(item) {
      if (item.path == $location.path()) {
        return true;
      }
      return false;
    };
  }])
  .controller('MyCtrl1', [function() {
  }])
  .controller('MyCtrl2', [function() {
  }]);

Now that the nav item data is being managed inside the controller, you can use ng-repeat directive to set the list items and isActive() to test for the path.

index.html

<html lang="en" ng-app="myApp">
<body>
  <ul ng-controller="NavCtrl" class="menu">
    <li ng-repeat="item in items" ng-class="{active: isActive(item)}"><a href="#{{item.path}}">{{item.title}}</a></li>
  </ul>
  <div ng-view></div>
  <script src="//ajax.googleapis.com/ajax/libs/angularjs/1.0.7/angular.min.js"></script>
</body>
</html>