Simple tabs directive: Angular transclusion, isolation and controller in action

Sometimes when there’s too much content for one page it’s convenient to hide some of it behind the tabs. Basically you can click on a tab to display one page or another. It comes quite handy and I use it a lot in many of my webapps. I came up with this handy directives that allows for dropping tabbed content pretty much anywhere on a page.

What I like about it is that even though it’s quite simple and concise it touches a lot of Angular concepts that believed to be complex and hard for beginners like transclusion and controller.

Interface is quite simple:

  <nge-tab name='books' title='Books'>
    <!-- Some content here -->
  <nge-tab name='reviews' title='Reviews'>
    <!-- Some content here -->

Let’s look under the hood

Here’s nge-tabs:

angular.module('readn.directives').directive 'ngeTabs', ->
    restrict: 'E'  # it only can be used as html element
    scope: {}     # isolated scope
    transclude: true  # content "falls through"
    replace: true  # nge-tabs itslef gets removed
    template: "..."
    controller: ['$scope', ($scope) ->
      $scope.tabs = []
      this.register = (name, title) ->
        $scope.tabs.push({name: name, title: title || name})
    link: ($scope, element, attrs) ->
      $ = $scope.tabs[0].name

      $scope.switch = (tab) ->
        $ =

And here is the template:

<div class="nge-tabs">
  <div class="tabs">
    <a ng-repeat='tab in tabs' ng-class="{active: active ==}" ng-click="switch(tab)">{{ tab.title }}</a>
   <div class="panes" ng-transclude></div>

See that ng-transclude thing? Basically it means that nge-tabs will take its inner content and put it inside this div.

Also as you can see the directive keeps track of all tabs (an empty array by default) and the active tab. Tabs are rendered as plain links, when you click one you invoke switch function which will update the active tab.

Now how the tabs array is initially populated?

Well, when you think about it, it’s obviously the job of child directive nge-tab to day “Hi, I’m here! Count me in”. To make it able to do so, we implement the controller method called register. It takes a name (which is a unique key) and an optional title and simply pushes them into tabs array. Easy enough.

No let’s see how the child nge-tab directives use this API.

angular.module('readn.directives').directive 'ngeTab', ->
    restrict: 'E'
    scope: {}
    require: '^ngeTabs',
    template: '<div class="page" ng-show="isVisible()" ng-transclude></div>'
    replace: true
    transclude: true
    link: ($scope, element, attrs, tabsCtrl) ->
      tabsCtrl.register(, attrs.title)

      $scope.isVisible = ->

First of all it register the tab with arguments taken from its attributes. It throws a simple div on a page with ng-show asking the parent whether it’s active or not.

If you’re not familiar with this concept yet, require: '^ngeTabs', means that we’re expecting it to be nested in nge-tabs directive. And now we are provided with it’s controller in link function (the fourth argument). We now can talk to it, tell it something (register) and ask questions (isActive).

Quite simple, isn’t it? There are two directives, pretty concise and self-explanatory. And yet we have a quite handy components we can use anywhere. This ability to hide complex stuff behind simple interfaces, this what I love Angular for.

P.S.: I run a weekly JavaScript round-up newsletter. So if you want to be updated with the latest JS news and articles, feel free to sign up.


Now read this

Why does clean code matter?

Surprisingly often I encounter this type of question in one form or the other. What’s the point of clean code? If the code has been written, and it does what it’s expected to (satisfying the business requirements), and doing this in a... Continue →