Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Set active tab style with AngularJS

A way to solve this without having to rely on URLs is to add a custom attribute to every partial during $routeProvider configuration, like this:

$routeProvider.
    when('/dashboard', {
        templateUrl: 'partials/dashboard.html',
        controller: widgetsController,
        activetab: 'dashboard'
    }).
    when('/lab', {
        templateUrl: 'partials/lab.html',
        controller: widgetsController,
        activetab: 'lab'
    });

Expose $route in your controller:

function widgetsController($scope, $route) {
    $scope.$route = $route;
}

Set the active class based on the current active tab:

<li ng-class="{active: $route.current.activetab == 'dashboard'}"></li>
<li ng-class="{active: $route.current.activetab == 'lab'}"></li>

One way of doing this would be by using ngClass directive and the $location service. In your template you could do:

ng-class="{active:isActive('/dashboard')}"

where isActive would be a function in a scope defined like this:

myApp.controller('MyCtrl', function($scope, $location) {
    $scope.isActive = function(route) {
        return route === $location.path();
    }
});

Here is the complete jsFiddle: http://jsfiddle.net/pkozlowski_opensource/KzAfG/

Repeating ng-class="{active:isActive('/dashboard')}" on each navigation tab might be tedious (if you've got many tabs) so this logic might be a candidate for a very simple directive.


Following Pavel's advice to use a custom directive, here's a version that requires adding no payload to the routeConfig, is super declarative, and can be adapted to react to any level of the path, by simply changing which slice() of it you're paying attention to.

app.directive('detectActiveTab', function ($location) {
    return {
      link: function postLink(scope, element, attrs) {
        scope.$on("$routeChangeSuccess", function (event, current, previous) {
            /*  
                Designed for full re-usability at any path, any level, by using 
                data from attrs. Declare like this: 
                <li class="nav_tab">
                  <a href="#/home" detect-active-tab="1">HOME</a>
                </li> 
            */

            // This var grabs the tab-level off the attribute, or defaults to 1
            var pathLevel = attrs.detectActiveTab || 1,
            // This var finds what the path is at the level specified
                pathToCheck = $location.path().split('/')[pathLevel] || 
                  "current $location.path doesn't reach this level",
            // This var finds grabs the same level of the href attribute
                tabLink = attrs.href.split('/')[pathLevel] || 
                  "href doesn't include this level";
            // Above, we use the logical 'or' operator to provide a default value
            // in cases where 'undefined' would otherwise be returned.
            // This prevents cases where undefined===undefined, 
            // possibly causing multiple tabs to be 'active'.

            // now compare the two:
            if (pathToCheck === tabLink) {
              element.addClass("active");
            }
            else {
              element.removeClass("active");
            }
        });
      }
    };
  });

We're accomplishing our goals by listening for the $routeChangeSuccess event, rather than by placing a $watch on the path. I labor under the belief that this means the logic should run less often, as I think watches fire on each $digest cycle.

Invoke it by passing your path-level argument on the directive declaration. This specifies what chunk of the current $location.path() you want to match your href attribute against.

<li class="nav_tab"><a href="#/home" detect-active-tab="1">HOME</a></li>

So, if your tabs should react to the base-level of the path, make the argument '1'. Thus, when location.path() is "/home", it will match against the "#/home" in the href. If you have tabs that should react to the second level, or third, or 11th of the path, adjust accordingly. This slicing from 1 or greater will bypass the nefarious '#' in the href, which will live at index 0.

The only requirement is that you invoke on an <a>, as the element is assuming the presence of an href attribute, which it will compare to the current path. However, you could adapt fairly easily to read/write a parent or child element, if you preferred to invoke on the <li> or something. I dig this because you can re-use it in many contexts by simply varying the pathLevel argument. If the depth to read from was assumed in the logic, you'd need multiple versions of the directive to use with multiple parts of the navigation.


EDIT 3/18/14: The solution was inadequately generalized, and would activate if you defined an arg for the value of 'activeTab' that returned undefined against both $location.path(), and the element's href. Because: undefined === undefined. Updated to fix that condition.

While working on that, I realized there should have been a version you can just declare on a parent element, with a template structure like this:

<nav id="header_tabs" find-active-tab="1">
    <a href="#/home" class="nav_tab">HOME</a>
    <a href="#/finance" class="nav_tab">Finance</a>
    <a href="#/hr" class="nav_tab">Human Resources</a>
    <a href="#/quarterly" class="nav_tab">Quarterly</a>
</nav>

Note that this version no longer remotely resembles Bootstrap-style HTML. But, it's more modern and uses fewer elements, so I'm partial to it. This version of the directive, plus the original, are now available on Github as a drop-in module you can just declare as a dependency. I'd be happy to Bower-ize them, if anybody actually uses them.

Also, if you want a bootstrap-compatible version that includes <li>'s, you can go with the angular-ui-bootstrap Tabs module, which I think came out after this original post, and which is perhaps even more declarative than this one. It's less concise for basic stuff, but provides you with some additional options, like disabled tabs and declarative events that fire on activate and deactivate.


@rob-juurlink I improved a bit on your solution:

instead of each route needing an active tab; and needing to set the active tab in each controller I do this:

var App = angular.module('App',[]);
App.config(['$routeProvider', function($routeProvider){
  $routeProvider.
  when('/dashboard', {
    templateUrl: 'partials/dashboard.html',
    controller: Ctrl1
  }).
  when('/lab', {
    templateUrl: 'partials/lab.html',
    controller: Ctrl2
  });
}]).run(['$rootScope', '$location', function($rootScope, $location){
   var path = function() { return $location.path();};
   $rootScope.$watch(path, function(newVal, oldVal){
     $rootScope.activetab = newVal;
   });
}]);

And the HTML looks like this. The activetab is just the url that relates to that route. This just removes the need to add code in each controller (dragging in dependencies like $route and $rootScope if this is the only reason they're used)

<ul>
    <li ng-class="{active: activetab=='/dashboard'}">
       <a href="#/dashboard">dashboard</a>
    </li>
    <li ng-class="{active: activetab=='/lab'}">
       <a href="#/lab">lab</a>
    </li>
</ul>