Angular Tabbed Navigation

Home / Angular Tabbed Navigation

In my previous posts regarding advanced Angular navigation, I showed how to create a menu and use the $state provider(s) to track the current state, which menu option is active, and how to move between states using ui-sref. This particular demo used an in-line service called “menuService.”

This is a pretty solid mechanism for navigation, but I wanted to also take a look at doing something similar with the same navigationService, ui-router, and ui-bootstrap tabs.


That demo, though, has evolved over time and I generally use a separate “navigationService,” which replaces the in-line “menuService,” and a list of states to display to the user. That navigationService becomes handy in that it’s reusable and works whether the navigation is based on tabs or a menu. I also put the states into a separate module so that the states can be injected.

However, this navigationService’s function is pretty thoroughly covered in that other blog post. I won’t bore you with that source code here. Suffice it to say, though, that most of the magic of that service is reliant on the ui-router’s state change events. The salient points, though, are that the navigationService provides this functionality:

  • Provides a list of all states
  • Indicates current state
  • Indicates previous
  • Provides a mechanism to let a controller tell the service whether or not navigation is allowed
  • Automatic dirty checking via callbacks to prevent navigation
  • Validation callback to prevent navigation if invalid

Let’s look at how we can easily integrate this service into a tabbed layout, though.

First, I define a service that incorporates a list of all states, or tabs. This service is pretty simple and looks like this:

var appStates = function ($state) {
    var
        states = [
            { name: 'state1', heading: "Tab 1", route: "tabs.state1", active: false, isVisible: true, href: $state.href("tabs.state1") },
            { name: 'state2', heading: "Tab 2", route: "tabs.state2", active: false, isVisible: true, href: $state.href("tabs.state2") },
            { name: 'state3', heading: "Tab 3", route: "tabs.state3", active: false, isVisible: true, href: $state.href("tabs.state3") },
            { name: 'state4', heading: "Tab 4", route: "tabs.state4", active: false, isVisible: true, href: $state.href("tabs.state4") }
        ];

    return {
        states: states
    };
};

You can see a simple array of states is returned. The navigationService will have this state service injected into it, mainly, to set whether a state is active.

For our routes, we’ll define a ui-route for each state. We’ll also define a top-level abstract state with a tab controller defined:

myApp.config(['$uibModalProvider', '$locationProvider', '$stateProvider', '$urlRouterProvider',
    function ($uibModalProvider, $locationProvider, $stateProvider, $urlRouterProvider) {
        $uibModalProvider.options = { animation: true, backdrop: 'static', keyboard: false };
        $locationProvider.html5Mode(false);

        $urlRouterProvider
            .when('/', '/state1')
            .otherwise("/state1");
            
        $stateProvider
            .state('tabs', {
                abstract: true,
                url: '/',
                views: {
                    'tabs': {
                        controller: 'tabsCtrl as tc',
                        templateUrl: 'tabs.html',
                    }
                }
            })
            .state('tabs.state1', {
                url: 'state1',
                templateUrl: 'state1.html',
                controller: function () { },
                reloadOnSearch: false
            })
            .state('tabs.state2', {
                url: 'state2',
                templateUrl: 'state2.html',
                controller: function () { },
                reloadOnSearch: false
            })
            .state('tabs.state3', {
                url: 'state3',
                templateUrl: 'state3.html',
                controller: function () { },
                reloadOnSearch: false
            })
            .state('tabs.state4', {
                url: 'state4',
                templateUrl: 'state4.html',
                controller: function () { },
                reloadOnSearch: false
            })
    }]);

The tabsCtrl is pretty simple. It contains a click-event handler a handle to the list of states available. That list of states will be used within an ng-repeat to display the tabs:

var tabsCtrl = function ($state, $location, $filter, appStates, navigationService) {
    var
        vm = this,            
        initialize = function () {
            vm.appStates = appStates.states;
        };
    vm.tabSelected = function (route) {
        $state.go(route);
    };
    initialize();
};

In our view, we define our abstract state like so:

<script type="text/ng-template" id="tabs.html">
    <div class="row">
        <uib-tabset>
            <uib-tab ng-repeat="tab in tc.appStates" heading="{{tab.heading}}" active="tab.active" disable="tab.disabled"
                        select="tc.tabSelected(tab.route)">
            </uib-tab>
        </uib-tabset>
    </div>
    
    <div id="tabs-views" data-ui-view></div>
</script>

You’ll notice we’re displaying the tabs simply via an ng-repeat and we define our ui-view within the abstract view’s template. With those pieces in place (navigationService, appStates, app definition), we’re pretty much finished. Our navigationService, click event handler, and other mechanisms will maintain state for us, and indicate the active tab. This lends itself to a nice stateful experience to the end-user.

One nuance worth mentioning is that the navigationService must be injected into the application prior to the instantiation of any controllers in order to ensure that the $stateChangeSuccess event is handled on first page load. It looks like this:

myApp.run(['$log', 'navigationService', function ($log, navigationService) {
    // Note, we need a reference to the navigationService so $state events are tracked.
    $log.log("Start.");
}]);

The full source can be seen on Github.
And, there’s a plunk if you prefer Plunker.

UPDATE: Since I wrote this blog post, angular-ui boostrap has changed a little bit. If you want to set the active tab, the “active” attribute on the tabset rather than the individual tabs must be set. It looks like this:

<uib-tabset active="tc.is.active">

The “active” attribute expects an index. It can be set like this in the controller’s initialize method comparing the current state to our list of states (or you can use the location change service’s events to get current state):

var criteriaFunction = function (c) {
    return c.route === $state.current.name;
};
var currentState = states.filter(criteriaFunction)[0];
var currentState = navigationService.currentState();
vm.is = { active: vm.areaStates.indexOf(currentState) };

Leave a Reply