Write AngularJS like a pro. Angularjs Icon

Follow the ultimate AngularJS roadmap.

Directive to Directive communication with require

Communication between Directives can be done in various ways. When dealing with Directives that have a hierarchical relationship we can use Directive Controllers to talk between them.

In this article we’ll build a tabs Directive that uses another Directive to add the tabs, using the require property of a Directive’s definition Object.

Table of contents

First let’s define the HTML:

<tabs>
  <tab label="Tab 1">
    Tab 1 contents!
   </tab>
   <tab label="Tab 2">
    Tab 2 contents!
   </tab>
   <tab label="Tab 3">
    Tab 3 contents!
   </tab>
</tabs>

We can assume at this point we need two Directives, tabs and tab. Let’s setup the base for tabs:

function tabs() {
  return {
    restrict: 'E',
    scope: {},
    transclude: true,
    controller: function () {
      this.tabs = [];
    },
    controllerAs: 'tabs',
    template: `
      <div class="tabs">
        <ul class="tabs__list"></ul>
        <div class="tabs__content" ng-transclude></div>
      </div>
    `
  };
}

angular
  .module('app', [])
  .directive('tabs', tabs);

For this tabs Directive, we’re going to use transclusion to pass each tab through and manage each tab individually.

Angular Directives In-Depth eBook Cover

Free eBook

Directives, simple right? Wrong! On the outside they look simple, but even skilled Angular devs haven’t grasped every concept in this eBook.

  • Green Tick Icon Observables and Async Pipe
  • Green Tick Icon Identity Checking and Performance
  • Green Tick Icon Web Components <ng-template> syntax
  • Green Tick Icon <ng-container> and Observable Composition
  • Green Tick Icon Advanced Rendering Patterns
  • Green Tick Icon Setters and Getters for Styles and Class Bindings

Inside the tabs Controller, we’ll need a function to add a new tab, so that our tabs are dynamically added to the parent/host Directive:

function tabs() {
  return {
    ...
    controller: function () {
      this.tabs = [];
      this.addTab = function addTab(tab) {
        this.tabs.push(tab);
      };
    },
    ...
  };
}

angular
  .module('app', [])
  .directive('tabs', tabs);

The Controller now has an addTab method bound. But how do we add a tab? We need to get started with adding the child tab Directive, and require its Controller functionality:

function tab() {
  return {
    restrict: 'E',
    scope: {
      label: '@'
    },
    require: '^tabs',
    transclude: true,
    template: `
     <div class="tabs__content" ng-if="tab.selected">
        <div ng-transclude></div>
      </div>
    `,
    link: function ($scope, $element, $attrs) {

    }
  };
}

angular
  .module('app', [])
  .directive('tab', tab)
  .directive('tabs', tabs);

We’ve successfully used require: '^tabs' to include the parent tabs Directive’s Controller, so we now have access to its functionality through the link function. Now we need to inject a fourth argument, $ctrl, to get our Controller reference:

function tab() {
  return {
    ...
    link: function ($scope, $element, $attrs, $ctrl) {

    }
  };
}

If we were to console.log($ctrl); we would see an Object similar to:

{
  tabs: Array,
  addTab: function addTab(tab)
}

Let’s utilise the addTab function and pass the newly created tab’s information back up the Directive into the parent Directive’s Controller:

function tab() {
  return {
    ...
    link: function ($scope, $element, $attrs, $ctrl) {
      $scope.tab = {
        label: $scope.label,
        selected: false
      };
      $ctrl.addTab($scope.tab);
    }
  };
}

Now, each time a new tab Directive is used, this $ctrl.addTab function is called and pushed into the this.tabs Array inside the tabs Controller.

With three tabs our $ctrl.addTab function will be called three times, and the Array will contain three items. We can then use the Array to iterate over the tab titles and add clickable behaviour to select the correct tab when each title is clicked:

function tabs() {
  return {
    restrict: 'E',
    scope: {},
    transclude: true,
    controller: function () {
      this.tabs = [];
      this.addTab = function addTab(tab) {
        this.tabs.push(tab);
      };
      this.selectTab = function selectTab(index) {
        for (var i = 0; i &lt; this.tabs.length; i++) {
          this.tabs[i].selected = false;
        }
        this.tabs[index].selected = true;
      };
    },
    controllerAs: &#039;tabs&#039;,
    template: `
      <div class="tabs">
        <ul class="tabs__list">
          <li ng-repeat="tab in tabs.tabs">
            <a href="" ng-bind="tab.label" ng-click="tabs.selectTab($index);"></a>
          </li>
        </ul>
        <div class="tabs__content" ng-transclude></div>
      </div>
    `
  };
}

You’ll now notice selectTab has been added to the tabs Controller. This allows us specify a tab’s index, which will then reveal that tab’s content. For instance this.selectTab(0); will set the first tab’s content to be displayed, as we’re using Array indexes to manage our logic.

Due to the Angular’s compiling procedure, the controller is instantiated first, then the link function once the Directive has been compiled and linked, which means to set an initial tab’s visibility we need to inject our Directive’s Controller using $ctrl and call the method there:

function tabs() {
  return {
    ...
    link: function ($scope, $element, $attrs, $ctrl) {
      // set the first tab to show first
      $ctrl.selectTab(0);
    },
  };
}

However, we might want to be more clever and allow an attribute to set the initial tab, giving the developer more flexibility:

<tabs active="2">
  <tab>...</tab>
  <tab>...</tab>
  <tab>...</tab>
</tabs>

This would set the tab at Array index 2 (so the third element in the Array). We can utilise $attrs in the link function to obtain the attribute’s presence, and set the index that way, or if the $attrs.active doesn’t exist or is falsy (0 evaluates to false so it’ll fall back to 0 anyway), it’ll fall back to setting the initial tab’s index.

function tabs() {
  return {
    ...
    link: function ($scope, $element, $attrs, $ctrl) {
      // set the active tab, or the first tab
      $ctrl.selectTab($attrs.active || 0);
    },
  };
}

And of course, the live demonstration of using require to pass new tab information back up into the parent Directive:

Learn Angular the right way.

The most complete guide to learning Angular ever built.
Trusted by 82,951 students.

Todd Motto

with Todd Motto

Google Developer Expert icon Google Developer Expert

Related blogs 🚀

Free eBooks:

Angular Directives In-Depth eBook Cover

JavaScript Array Methods eBook Cover

NestJS Build a RESTful CRUD API eBook Cover