Deep Linking a Tabbed UI With AngularJS

Monday, April 14, 2014

The idea is to dynamically generate a tabbed navigation using Angular and UI Bootstrap.

 I’ve done this before, but this time around I needed the ability to deep link into a tab. That is, if a user bookmarks /someapp/tab2, then the 2nd tab should be active with its content showing.

Tabbed UI Angular JS

Instead of using ngRouter, which is a bit simplistic, I decided to use UI Router. UI Router is not without quirks and bugs, but it does give the opportunity to setup multiple, named “states” for an application, and can manage nested states and routes through associated URLs. One of the first steps in working with UI Router is configuring the known states

var app = angular.module("routedTabs", ["ui.router", "ui.bootstrap"]);

app.config(function($stateProvider, $urlRouterProvider){

    $urlRouterProvider.otherwise("/main/tab1");

    $stateProvider
        .state("main", { abstract: true, url:"/main", templateUrl:"main.html" })
            .state("main.tab1", { url: "/tab1", templateUrl: "tab1.html" })
            .state("main.tab2", { url: "/tab2", templateUrl: "tab2.html" })
            .state("main.tab3", { url: "/tab3", templateUrl: "tab3.html" });
});

In the above code, “main” is a parent state with three children (tab1, tab2, and tab3). Each child has an associated URL (which will be appended to the parent URL) and a template. Each child template will plug into the parent template of main.html, which itself has to plug into the application shell.

In other words, the shell of the application uses the ui-view directive to position the parent template (main.html).

<body ng-app="routedTabs" class="container">

   <div ui-view></div>

</body>

This is not much different than using ngRouter and its ngView directive, but UI router also allows for main.html to use another ui-view directive where one of the child templates will appear.

<div ng-controller="mainController">

    <tabset>
        <tab 
            ng-repeat="t in tabs" 
            heading="{{t.heading}}"
            select="go(t.route)"
            active="t.active">
        </tab>
    </tabset>

    <h2>View:</h2>
    <div ui-view></div>

</div>

This view requires a controller to provide the tab data.

app.controller("mainController", function($rootScope, $scope, $state) {        
   
    $scope.tabs = [
        { heading: "Tab 1", route:"main.tab1", active:false },
        { heading: "Tab 2", route:"main.tab2", active:false },
        { heading: "Tab 3", route:"main.tab3", active:false },
    ];

    $scope.go = function(route){
        $state.go(route);
    };

    $scope.active = function(route){
        return $state.is(route);
    };

    $scope.$on("$stateChangeSuccess", function() {
        $scope.tabs.forEach(function(tab) {
            tab.active = $scope.active(tab.route);
        });
    });
});

The only reason to listen for UI router’s $stateChangeSuccess is to keep the right tab highlighted if the URL changes. It’s a bit of a hack and actually makes me wonder if using tabs from UI Bootstrap is worth the extra code, or if it would be easier to write something custom and integrate directly with UI router.

If you want to try the code for yourself, here it is on Plunkr


Comments
gravatar Rajan Tuesday, April 15, 2014
informative
gravatar ACanadian Saturday, April 19, 2014
Great little article. For me deep linking into a tabbed UI becomes useful when the tabs are added dynamically by page and key. In other words, when the user navigates to main/tab2, then that tab is added to the UI. If you had more complex URL, like: main/tab2?key=1. Then you could potentially add tab 2 to the UI for each unique key. This is how I have a project setup, it is just not tied into to routing.
tmack Wednesday, April 23, 2014
Have you seen ui-sref-active? http://angular-ui.github.io/ui-router/site/#/api/ui.router.state.directive:ui-sref-active Dunno if you can get it to work with ui.bootstrap tabs, but works great with a tags with ui-sref.
gravatar scott Wednesday, April 23, 2014
@tmack: I had problems with the tab markup and sref-active, IIRC this was because with tabs the enclosing li element has to have the class of active.
gravatar jerryk Thursday, April 24, 2014
I have an app like this based on UI-Router and it works find in the latest version of browsers, but fails in IE8, Have you ever tried yours in IE8. Does it work? What I see is the correct page content flashes for a second and then the base page flashes. In my case the base page is an aspx web for and I see it go all that way back to the inital pageLoad() event.
gravatar scott Saturday, April 26, 2014
@jerryk: I haven't tried. I've been fortunate to work on a project with IE9+
gravatar Michele Roma Friday, May 9, 2014
Error: Could not resolve 'main.tab1' from state '' var mainApp = angular.module('mainApp', ["ui.router", "ui.bootstrap", 'ngRoute', 'commessasControllers', 'commessasServices', 'menuDirectives', 'menuControllers']); mainApp.config(function ($routeProvider, $stateProvider) { $routeProvider. when('/', { templateUrl: 'scripts/app/views/home/Home.html' }). when('/commessas', { templateUrl: 'scripts/app/views/commessas/Commessas.html', controller: 'commessasListController' }). when('/main', { templateUrl: 'scripts/app/views/commessas/shared/main.html' }). when('/commessaEdit/:commessaId', { templateUrl: 'scripts/app/views/commessas/CommessaEdit.html', controller: 'commessaEditController' }). otherwise({ redirectTo: '/' }); $stateProvider .state("main.tab1", { url: "/tab1", templateUrl: "tab1.html" }) .state("main.tab2", { url: "/tab2", templateUrl: "scripts/app/views/commessas/tab2.html" }) .state("main.tab3", { url: "/tab3", templateUrl: "scripts/app/views/commessas/tab3.html" }); commessasControllers.controller("mainController", function ($rootScope, $scope, $state) { $scope.go = function (route) { alert(route); $state.go(route); }; $scope.active = function (route) { return $state.is(route); }; $scope.tabs = [ { heading: "Tab 1", route: "main.tab1", active: false }, { heading: "Tab 2", route: "main.tab2", active: false }, { heading: "Tab 3", route: "main.tab3", active: false }, ]; $scope.$on("$stateChangeSuccess", function () { $scope.tabs.forEach(function (tab) { tab.active = $scope.active(tab.route); }); }); });
mikeb Saturday, May 10, 2014
If sref-active doesn't work with ui.bootstrap's tabs, couldn't you add the $state to the $scope, and then use ng-class on the li?
gravatar Scott Saturday, May 10, 2014
@Michele: It looks like you are mixing ngRoute and uiRoute rules, I'm not sure if that can work but I'd try to avoid it. @mikeb - Yes, good idea, I'm sure that could work, too.
gravatar Michele Monday, May 12, 2014
hi Scott and mikeb , I thank you for your answers. Please can you see the problem in plunkr? http://plnkr.co/edit/iqxWNPR6r5bqReN4JrBs?p=preview the 'NEW' link should show the tabs.... thanks
gravatar Emanuel Wednesday, May 21, 2014
I've tried to add controllers for each tab view and it doesn't work for me. Can you explain how to do that using your code? I did like this: .state("main.tab1", { url: "/tab1", templateUrl: "tab1.html", controller: "tab1Ctrl" }) thank you
gravatar Emanuel Wednesday, May 21, 2014
Sorry my mistake, it works perfectly :) Thank you very much
Comments are now closed.
by K. Scott Allen K.Scott Allen
My Pluralsight Courses
The Podcast!