Shaun Xu

The Sheep-Pen of the Shaun


News

logo

Shaun, the author of this blog is a semi-geek, clumsy developer, passionate speaker and incapable architect with about 10 years’ experience in .NET and JavaScript. He hopes to prove that software development is art rather than manufacturing. He's into cloud computing platform and technologies (Windows Azure, Amazon and Aliyun) and right now, Shaun is being attracted by JavaScript (Angular.js and Node.js) and he likes it.

Shaun is working at Worktile Inc. as the chief architect for overall design and develop worktile, a web-based collaboration and task management tool, and lesschat, a real-time communication aggregation tool.

MVP

My Stats

  • Posts - 122
  • Comments - 577
  • Trackbacks - 0

Tag Cloud


Recent Comments


Recent Posts


Archives


Post Categories


.NET



Bootstrap provides a component named Tabs, which provides tab container and tab pages. In order to make it easy to use in Angular.js, Angular UI team created a directive called "ui.bootstrap.tabs". By using this directive we can define a bootstrap tab component, we can also specify the content in each tab pages. But the problem is, all tabs' content must be in the same scope. For example, below is the sample code from "ui.bootstrap.tabs". As we can see, I must put all tabs' content in the same page, and using the same scope.

   1: <tabset>
   2:   <tab heading="Static title">Static content</tab>
   3:   <tab ng-repeat="tab in tabs" heading="{{tab.title}}" active="tab.active" disable="tab.disabled">
   4:     {{tab.content}}
   5:   </tab>
   6:   <tab select="alertMe()">
   7:     <tab-heading>
   8:       <i class="glyphicon glyphicon-bell"></i> Alert!
   9:     </tab-heading>
  10:     I've got an HTML heading, and a select callback. Pretty cool!
  11:   </tab>
  12: </tabset>

 

In my project, I would like to have a page with a tab container, but each tab's template (HTML) and business logic (controller) can be defined separately. Something looks like this.

   1: <tabset>
   2:     <tab template="tabs/tab-1.html" controller="Tab1Ctrl"></tab>
   3:     <tab template="tabs/tab-2.html" controller="Tab2Ctrl"></tab>
   4:     <tab template="tabs/tab-3.html" controller="Tab3Ctrl"></tab>
   5: </tabset>

And this component should be smart enough to load content and execute business logic when user active this tab. So this is the reason I created this directive.

 

Features

- Tab's content can be defined through inline HTML string or an external HTML file.

- Tab's controller can be specified inline or by name.

- Each tab contains its own scope, but can share objects through "$scope.$context.data" property.

- Tabs can be close or open.

- Tab's content and logic is lazy load (render and execute only when the tab was shown).

- Provides some handlers that can be hooked when a tab was opened, closed, entered and left.

ezgif.com-optimize

 

Installation & 1st Tab

First you need to reference tow files from GitHub.

https://github.com/shaunxu/angular-toolkits/blob/master/tabs/sx-tabs-tpls.js

https://github.com/shaunxu/angular-toolkits/blob/master/tabs/sx-tabs.js

 

Then you need to reference some basic libraries I think you have done before.

- jQuery

- Bootstrap

- Angular.js

 

In your Angular.js application module, specify the dependency of "sx.tabs" as below.

   1: window.app = window.angular.module('Demo', [
   2:     'YOUR-DEPENDENCY-MODULE', 
   3:     'YOUR-DEPENDENCY-MODULE', 
   4:     ... ...
   5:     'YOUR-DEPENDENCY-MODULE', 
   6:     'sx.tabs'
   7: ]);

And now you can define tabs in your page as below.

   1: <div sx-tabs="tabs" 
   2:      sx-tabs-context="context" 
   3:      sx-tabs-enabled="onTabEnabled(tab)" 
   4:      sx-tabs-disabled="onTabDisabled(tab)" 
   5:      sx-tab-switched="onTabSwtiched(tab)">
   6: </div>

The meaning of the attributes are described as following.

- sx-tabs: Tabs' definition. Details will be discussed later.

- sx-tabs-context: Data will be passed into each tabs' controller scope. It will be shared between all tabs.

- sx-tabs-enabled: A function will be invoked when a tab was shown from the "more" button.

- sx-tabs-disabled: A function will be invoked when a tab was hidden into the "more" button.

- sx-tabs-switched: A function will be invoked when a tab was activated.

 

Next, you need to define tabs in your controller scope, with the property name specified in previous step. In this case it's "$scope.tabs". In the code below I defined a tab with ID = "general".

   1: var tabs = {
   2:     'general': {
   3:         id: 'general',
   4:         title: 'General',
   5:         order: 1110,
   6:         enabled: true,
   7:         templateUrl: 'tabs/general.html',
   8:         controller: 'sxTabGeneralCtrl'
   9:     }
  10: };
  11: $scope.tabs = tabs;

Each tab definition must contain properties as below.

- id: A unique string to identify this tab in this tab container.

- title: Tab title that will be displayed in the tab bar.

- order: Defines the display sequence of tabs. Tab with lower order will be displayed on the left.

- enabled: Tab will be shown by default when it's true, otherwise it will be hidden in "more" button.

- templateUrl: HTML path for the tab content.

- template: Inline HTML string for the tab content. This value will override "templateUrl".

- controller: Inline function or controller name of this tab. This property is optional.

 

In "sx.tabs" each tab has its own controller and scope, that you can specify your own business logic in each tab without messing them up. In this tab we defined "$scope.productName" in controller.

   1: (function () {
   2:     window.app.controller('sxTabGeneralCtrl', function ($scope) {
   3:         $scope.productName = 'sx.tabs';
   4:     });
   5: }())

And display in "tabs/general.html".

   1: <div class="jumbotron" style="background-color: #fff;">
   2:   <h1>Hello, tabs!</h1>
   3:   <p>Welcome to use "{{productName}}" in Shaun's Angular Toolkits</p>
   4: </div> 

Now the tab will look like this. It contains one tab with almost no logic.

image

 

Shared Data

As I mentioned above, you can pass objects into tabs and it will be shared across all tabs. To do this, you need to define the data in parent controller scope and pass through the "sx-tabs-context" attribute. For example below is the data.

   1: var context = {
   2:     user: 'Shaun',
   3:     company: 'Worktile Inc.',
   4:     department: 'Web'
   5: };
   6: $scope.context = context; 

And specify the tab context through its directive attribute named "sx-tabs-context". Then I can retrieve this object from tab's controller scope through "$scope.$context.data". For example below I created another tab.

Note that in this tab I didn't specify controller, since it doesn't need any logic.

   1: var tabs = {
   2:     'general': {
   3:         ... ...
   4:     },
   5:     'shared-data': {
   6:         id: 'shared-data',
   7:         title: 'Shared Data',
   8:         order: 1120,
   9:         enabled: true,
  10:         templateUrl: 'tabs/shared-data.html'
  11:     }
  12: }

And in the template HTML and retrieve my shared data from "$scope.$context.data" and bind with several inputs.

   1: <form>
   2:   <div class="form-group">
   3:     <label for="user">User</label>
   4:     <input type="text" class="form-control" id="user" placeholder="User" ng-model="$context.data.user">
   5:   </div>
   6:   <div class="form-group">
   7:     <label for="company">Company</label>
   8:     <input type="text" class="form-control" id="company" placeholder="Company" ng-model="$context.data.company">
   9:   </div>
  10:   <div class="form-group">
  11:     <label for="dept">Department</label>
  12:     <input type="text" class="form-control" id="dept" placeholder="Department" ng-model="$context.data.department">
  13:   </div>
  14: </form>

Now we have two tabs and in the second one, you can see the data I defined in parent scope was passed into tab scope.

image

And since the are referring the same object, any changes inside one tab will affect all other tabs as well as the parent scope.

image

 

Tab Entering & Leaving

A tab will be rendered and performs its controller when the first time it was shown, no matter by clicking the tab title, or from the "more" button. This enhance the performance of the whole tab container. But it will never be rendered again.

In some cases we need the tab to load some data and update the page when it's switched. What we can do is to implement our own tab entering function.

To show this feature I created another tab, which was similar as the previous one.

   1: var tabs = {
   2:     'general': {
   3:         ... ...
   4:     },
   5:     'shared-data': {
   6:         ... ...
   7:     },
   8:     'enter-validate': {
   9:         id: 'enter-validate',
  10:         title: 'Load & Validation',
  11:         order: 1150,
  12:         enabled: true,
  13:         templateUrl: 'tabs/enter-validate.html',
  14:         controller: 'sxTabEnterValidateCtrl'
  15:     }
  16: }

In the controller I defined a function inside "$scope.$context.behavior" called "entering". It will be invoked when this tab was activated, with two arguments "options", and "callback".

The "options" contains information about this entering event as below.

- fromTabId: The ID of which tab switched from. It might be null if this is the first visible tab.

- entered: Indicates whether this tab had been shown before. It can be used to avoid loading data duplicated.

The "callback" argument is a function that MUST be invoked when entering logic was finished, regardless if it's success or failed.

Below is the code I implemented in my tab. It utilizes "$timeout" to simulate async loading. It also checked "options.entered" to ensure load data only when this tab was entered first time.

   1: (function () {
   2:     window.app.controller('sxTabEnterValidateCtrl', function ($scope, $timeout) {
   3:         $scope._entering = false;
   4:         
   5:         $scope.$context.behavior.entering = function (options, callback) {
   6:             $scope._entering = true;
   7:             if (options.entered) {
   8:                 $scope._entering = false;
   9:                 return callback ();
  10:             }
  11:             else {
  12:                 $timeout(function () {
  13:                     $scope._entering = false;
  14:                     return callback();
  15:                 }, 1000);
  16:             }
  17:         };
  18:     });
  19: }())

 

Similar as entering handler, we can implement "$scope.$context.behavior.leaving" to perform our own logic when a tab is going to be left. This could be useful if we want to prevent user from leaving this tab if any inputs were invalid.

"leaving" function also contains "options" and "callback" arguments. The details of "options" is

- toTabId: The ID of which tab it will be switched to.

- byTabDisable: A boolean value to indicates whether this leaving is because user hide this tab.

Same as "entering", "callback" function MUST be invoked when leaving logic was finished. It contains one arguments to indicates if it can be left or not. For example, if validation was failed, we need to call "return callback(false)" to prevent user leaving this tab.

Below is the code I implemented in the sample tab. I utilizes Angular.js form validation module to check if user can left this tab.

   1: $scope.$context.behavior.leaving = function (options, callback) {
   2:     $scope._leaving = true; 
   3:  
   4:     if (options.byTabDisable) {
   5:         $scope._leaving = false;
   6:         return callback(true);
   7:     }
   8:     else {
   9:         $timeout(function () {
  10:             $scope._leaving = false;
  11:             return callback($scope.inputForm.$valid);
  12:         }, 1000);
  13:     }
  14: };

Now if you clear any inputs in this tab you cannot switch to any other tabs.

image

 

Show & Hide Tabs

Tabs can be hidden by clicking its close icon and can be shown again from the "more" button.

image

We can also define a tab which will be hidden by default. Just set its "enabled" property to "false". For example in the code below I defined 3 tabs with "enabled: false".

   1: var tabs = {
   2:     'general': {
   3:         ... ...
   4:     },
   5:     'shared-data': {
   6:         ... ...
   7:     },
   8:     'enter-validate': {
   9:         ... ...
  10:     },
  11:     'more-tabs-1': {
  12:         id: 'more-tabs-1',
  13:         title: 'More Tabs 1',
  14:         order: 1210,
  15:         enabled: false,
  16:         template: '<h1>More Tabs 1</h1>'
  17:     },
  18:     'more-tabs-2': {
  19:         id: 'more-tabs-2',
  20:         title: 'More Tabs 2',
  21:         order: 1220,
  22:         enabled: false,
  23:         template: '<h1>More Tabs 2</h1>'
  24:     },
  25:     'more-tabs-3': {
  26:         id: 'more-tabs-3',
  27:         title: 'More Tabs 3',
  28:         order: 1230,
  29:         enabled: false,
  30:         template: '<h1>More Tabs 3</h1>'
  31:     }
  32: };

Then it will be hidden.

image

 

You can also hook the events when user show, hide a tab, as well as when a tab was switched. This might be useful if you want to save the tab status into user's preference so that next time user can see tabs she opened previously.

To accomplish just specify which function these events will invoke.

   1: <div sx-tabs="tabs" 
   2:      sx-tabs-context="context" 
   3:      sx-tabs-enabled="onTabEnabled(tab)" 
   4:      sx-tabs-disabled="onTabDisabled(tab)" 
   5:      sx-tab-switched="onTabSwtiched(tab)">
   6: </div>

And implement those functions in your parent scope as below.

   1: $scope.onTabEnabled = function (tab) {
   2:     $scope.messages.push('Tab enabled: ' + tab.id);
   3: };
   4: $scope.onTabDisabled = function (tab) {
   5:     $scope.messages.push('Tab disabled: ' + tab.id);
   6: };
   7: $scope.onTabSwtiched = function (tab) {
   8:     $scope.messages.push('Tab switched: ' + tab.id);
   9: };

Then in the page you can see events fired from the directive. You can hook any of them and perform you logic.

image

 

Promise Support

The last feature I would like to mention, this directive support specify "tabs" and "context" as a promise. This is very useful when the tab definition and shared data must be retrieved asynchronous.

For example, I utilizes "$timeout" to simulate the case that retrieve "context" asynchronous.

   1: var context = {
   2:     user: 'Shaun',
   3:     company: 'Worktile Inc.',
   4:     department: 'Web'
   5: };
   6: $scope.context = $q(function (resolve, reject) {
   7:     $timeout(function () {
   8:         return resolve(context);
   9:     }, 1000);
  10: });

Similarly, "tabs" can be a promise as well.

   1: $scope.tabs = $q(function (resolve, reject) {
   2:     $timeout(function () {
   3:         return resolve(tabs);
   4:     }, 1000);
   5: });

The directive is smart enough to handler promise case, or normal object case.

 

Summary

In this post I introduced an Angular.js directive I created and am using in my project, which is a wrapper of Bootstrap tabs with more customization features.

Different from the tabs implemented in UI-Bootstrap, you can define tab's template and controller separately. And it will be loaded, rendered when the tab was activated. You can also pass objects that can be shared through all tabs, but each tab scope are isolated. It supports customize entering and leaving function, tab show, hide and switch function. It also accept tab definition and shared data as object or promise.

You can find the online demo at this plunker. You can find the source code in GitHub. If you found any bugs or any feature requirements please feel free to open issues.

 

Hope this helps,

Shaun

All documents and related graphics, codes are provided "AS IS" without warranty of any kind.
Copyright © Shaun Ziyan Xu. This work is licensed under the Creative Commons License.

Comments

Gravatar # re: Angular Toolkits: Customizable Bootstrap Tabs
Posted by Patxi on 1/4/2016 9:52 PM
Very good!
How I can use ControllerAs?
Gravatar # re: Angular Toolkits: Customizable Bootstrap Tabs
Posted by Johnny Driesen on 2/29/2016 3:23 PM
Hi Sir,
Thanks for this great explanation ...
I'm having one more question.

How can I force to set a tab 'active' by pressing a button ?

Thanks in advance for your answer,

Johnny
Post A Comment
Title:
Name:
Email:
Comment:
Verification: