Bootstrap's Tabs and Lazy Data Loading in AngularJS

Arjan Wulder

AngularJSThere are a lot of great sets of directives available that are based on Twitter Bootstrap's markup and CSS and AngularJS. Some examples are AngularStrap and AngularUI, but neither of them has a directive that supports the Bootstrap's tabs and lazy data loading. So when the page with the tab directive is initially loaded, all the data, even of the not-active tabs, are loaded.

Here is how I solved the lazy data loading with Twitter Bootstrap's markup and AngularJS.

Tabset/Tab directives

The most interesting part is the code, so here it is. At first sight these are pretty straightforward directives. Though you probably can't see where the lazy data loading is done, but it's there! I assume you know how to create directives so I will explain only how the lazy loading is done. If you want to know more about AngularJS directives you can visit the AngularJS docs.

The Directives - JavaScript

'use strict';

angular.module('bootstrap.tabset', [])
.directive('tabset', function () {
  return {
    restrict: 'E',
    replace: true,
    transclude: true,
    controller: function($scope) {
      $scope.templateUrl = '';
      var tabs = $scope.tabs = [];
      var controller = this;

      this.selectTab = function (tab) {
        angular.forEach(tabs, function (tab) {
          tab.selected = false;
        });
        tab.selected = true;
      };

      this.setTabTemplate = function (templateUrl) {
        $scope.templateUrl = templateUrl;
      }

      this.addTab = function (tab) {
        if (tabs.length == 0) {
          controller.selectTab(tab);
        }
        tabs.push(tab);
      };
    },
    template:
      '<div class="row-fluid">' +
        '<div class="row-fluid">' +
          '<div class="nav nav-tabs" ng-transclude></div>' +
        '</div>' +
        '<div class="row-fluid">' +
          '<ng-include src="templateUrl">' +
        '</ng-include></div>' +
      '</div>'
  };
})
.directive('tab', function () {
  return {
    restrict: 'E',
    replace: true,
    require: '^tabset',
    scope: {
      title: '@',
      templateUrl: '@'
    },
    link: function(scope, element, attrs, tabsetController) {
      tabsetController.addTab(scope);

      scope.select = function () {
        tabsetController.selectTab(scope);
      }

      scope.$watch('selected', function () {
        if (scope.selected) {
          tabsetController.setTabTemplate(scope.templateUrl);
        }
      });
    },
    template:
      '<li ng-class="{active: selected}">' +
        '<a href="" ng-click="select()">{{ title }}</a>' +
      '</li>'
  };
});

HTML

This is a HTML example of how you can use the directives.

<tabset>
  <tab title="Tab 1" template-url="/views/tab1.html"></tab>
  <tab title="Tab 2" template-url="/views/tab2.html"></tab>
  <tab title="Tab 3" template-url="/views/tab3.html"></tab>
</tabset>

What About Lazy Data Loading?

The trickiest part was to figure out the lazy loading part. There isn't a way in AngularJS to say that you want lazy loading enabled. I solved the lazy loading part by using Angular's ng-include directive. Each time a tab is clicked the selected property of that tab is changed and it will eventually invoke the setTabTemplate function that will set the src attribute of the ng-include directive. When a ng-include without a src attribute is loaded, AngularJS will load nothing. When the src attribute is set, it will load the template. In the case of the tabs it will load the template of the selected tab. This is how I solved the lazy loading part.

Finally

Though there is no out-of-the-box option in AngularJS for enabling lazy loading, you can achieve it by using the ng-include directive. I showed one approach but there are probably other approaches as well.

I have also added a working example that demonstrates that the templates are only loaded when they are needed. Please see angularjs-tabs-and-lazy-data-loading on Github.

Comments (15)

  1. Tyler - Reply

    September 3, 2013 at 11:55 pm

    Will this though re-load the data each time the tab is accessed?

  2. Ismael - Reply

    September 25, 2013 at 1:07 pm

    I don't understand if this is good.

    this.setTabTemplate = function (templateUrl) {
    $scope.templateUrl = templateUrl;
    }

    You could do directly:
    $scope.templateUrl = templateUrl;

    Since you can access all $scope variables anyway.

    I tried do get rid of this and i could not. Don't seems to be a good practice.

    • Arjan Wulder - Reply

      September 25, 2013 at 1:39 pm

      You can't access the parent scope by using $scope because the tab directive creates an isolated scope. An isolated scope doesn't prototypically inherit from the parent scope so that's the the reason why you can't use $scope for accessing the parent scope.

      However, you can access the parent scope by using element.parent().parent().scope() and then set the templateUrl so you can remove the setTabTemplate method in the tabset controller, but I think it's less readable and an isolated scope prevents accidentally reading or modifying data in the parent scope.

      Here is a Plunker without the setTabTemplate method, I'm curious what approach you prefer? http://plnkr.co/edit/djIfXMLK1fRlT3MBp8Ls?p=preview

  3. SK - Reply

    March 26, 2014 at 1:14 am

    Thanks for the directive. but when i try to use the routeProvider on each click, there are multiple submissions happening on each click.

    In my controller i have a locationPath(); defined.

  4. Miguel Ángel Garzón - Reply

    May 23, 2014 at 1:10 pm

    Excellent, works like a charm.

    I've been able to change the template using Semantic UI Menu Element without tab behaviors and works even better and simpler than Semantic UI Menu Element with tabs behaviors.

    Thanks!

  5. ahelmel - Reply

    August 25, 2014 at 1:50 pm

    Very nice directive, but how can i test the methods selectTab, setTabTemplate and addTab of the controller of the tabset directive?

  6. ChrisP - Reply

    August 27, 2014 at 10:51 am

    This is unfortunately not working for me. It seems the link method of the tab directive is not getting called. Anyone know why that is?

  7. david - Reply

    September 29, 2014 at 3:32 pm

    hello Mr.Wulder
    Your example is really great.
    Very thenks.

  8. John - Reply

    October 29, 2014 at 6:18 pm

    ChrisP.... I believe that the issue is that your declaration is not placed as an internal element for a controller, thus you are most likely without a scope or are using the wrong scope.

    .....

  9. Deepesh - Reply

    November 14, 2014 at 4:52 pm

    @Arjan: Cool directive. But I am not able to use ui-bootstrap along with this directive.
    once I am trying to inject "ui-bootstrap" along with this, Error: [$injector:modulerr] Failed to instantiate module myApp.
    {code} var app = angular.module('myApp', ['bootstrap.tabset', 'ui-bootstrap']); {code}

    Can you please provide your guidance to use both together.

  10. Deepesh - Reply

    November 14, 2014 at 4:54 pm

    @Arjan: Cool directive. But I am not able to use ui-bootstrap along with this directive.
    once I am trying to inject "ui-bootstrap" along with this, Error: [$injector:modulerr] Failed to instantiate module myApp.

    Using following code :
    var app = angular.module('myApp', ['bootstrap.tabset', 'ui-bootstrap']); {code}

    Can you please provide your guidance to use both together.

    • Manoj Tyagi - Reply

      December 19, 2014 at 8:11 am

      Initially I was also not able to use this directive with ui-bootstrap, then what I did , I just renamed tag names and it started working. For example in my case I used it as follows

      <mytabs >
      <mytab title="Tab 1" template-url="/views/tab1.html"></mytab>
      <mytab title="Tab 2" template-url="/views/tab2.html"></mytab>
      <mytab title="Tab 3" template-url="/views/tab3.html"></mytab>
      </mytabs >

      Accordingly I made changes inside directive's javascript code(replace tabset with mytabs & tab with mytab) and things started working for me. Please let me know if still you face the issue or your issue got resolved.

  11. Manoj Tyagi - Reply

    December 18, 2014 at 5:23 pm

    Initially I was also not able to use this directive with ui-bootstrap, then what I did , I just renamed tag names and it started working. For example in my case I used it as follows

    Accordingly I made changes inside directive's javascript code(replace tabset with mytabs & tab with mytab) and things started working for me. Please let me know if still you face the issue or your issue got resolved.

Add a Comment