Communication Between Directives in AngularJS

December 17th, 2012

Warning: This article is long and thorough. If you’re in a hurry, you can skip straight to the final product here:

http://plnkr.co/edit/6KxHY8ZpE83Z2PeNWlYi?p=preview

Prerequisites to this article

Before reading this, get to know these AngularJS terms. You don’t need too much depth, but you need to know what they are:

You should also peruse the Angular Developer Guide, but don’t feel bad if a lot of the jargon does not make much sense at first.

Step 0: Roadmap

We’re going to create two custom directives that communicate with each other. We’ll really dig deep into the first directive, explaining every line of code as we go.

Then we’ll glue the directive to a controller via a scope object.

Lastly, we’ll create a second directive and glue it all together.

Step 1: A basic directive, dissected

For this example, we’ll create a custom search box that can be placed in our app with a custom <my-search-box> element. This directive causes Angular to convert <my-search-box> in your HTML into <span>My Custom Search Box</span>. This is not useful yet, but it will become immensely useful as we grow this concept in a few paragraphs.

We’ll create a file called “app.js” with this content:

angular.module("MyApp", []).
  directive('mySearchBox', function() {
    return {
      restrict: 'E',
      replace: true,
      template: '<span>My Custom Search Box</span>'
    };
  });

Let’s walk through each line of that directive:

angular.module("MyApp", []).

This line declares a new module called “MyApp”. The empty array to the right can contain the names of other modules on which this module depends (this is where you would put AngularUI modules for example if you want to use AngularUI). In this case, this module happens to be our “main” application module, but Angular really doesn’t know that from this line of code. That happens in the HTML with <html ng-app="MyApp">. Also, you only declare your main application module once, even if you have many directives. There are ways to look up your module if your code spans multiple files, or you can just create a global variable to reference your module and use that when building your directive.

  directive('mySearchBox', function() {

Here we are declaring a new directive. Notice that we camel case the directive name. When Angular encounters <my-search-box> in the HTML, it will normalize that into “mySearchBox” to look up the directive. Thus, “mySearchBox” is called the “normalized” name.

    return {

Angular expects the directive function to return an object with properties that describe the directive.

      restrict: 'E',

This tells Angular that “my-search-box” must be an element. Angular directives can also exist as HTML attributes, HTML class names, and even HTML comments (!). It’s okay if this doesn’t make sense yet.

      replace: true,

This tells Angular to fully replace <my-search-box> with the directive’s template HTML. Alternatively, Angular can add the directive’s template HTML to the <my-search-box> element as a child element. For our example, we want Angular to fully replace <my-search-box> because <my-search-box> is not a valid HTML element name. You can imagine wanting Angular to simply augment an element with child elements in some scenarios, but not this example. We’re effectively creating a “widget” that can be instantiated and reused in our HTML.

      template: '<span>My Custom Search Box</span>'

This is the directive’s HTML template that will be rendered by the browser. I don’t recommend using hard-coded template strings in your directives like I’ve done here. I do it this way to make the example clear. Normally, I use the templateUrl property and serve the HTML as a “partial” from my web server. Angular caches these partials, and you can even pre-load them when your app loads. This way, I can use server-side template technologies for things like localization. Although, for the best performance, I recommend that you serve these partials statically from a CDN or other fast caching service.

Now we can use this directive in our HTML like this:

<html ng-app="MyApp">
  <head>
    <script src="http://ajax.googleapis.com/ajax/libs/angularjs/1.0.3/angular.min.js"></script>
    <script src="app.js"></script>
  </head>
  <body> 
      <my-search-box></my-search-box>
  </body>
</html>

When you visit that page in your browser, you’ll see “My Custom Search Box” if all went well above.

So far this is a pretty useless directive, but we’are about to take it to the next level.

Step 2: Giving your directive its own scope

Each instance of a directive has its own scope. And of course, the scope is a child of the enclosing scope where you instantiated the directive in the HTML. For example, if your directive exists inside a <div ng-controller="MyController">, then your directive’s scope will be a child of MyController‘s $scope.

For example, the following HTML uses two <my-search-box> elements. Angular gives each <my-search-box> its own independent scope:

  <body> 
      <my-search-box></my-search-box>
      <my-search-box></my-search-box>
  </body>

Let’s define our directive’s interface to the outside world using the scope property. Our interface will have two values: searchText and isSearching. We will use searchText to store the user’s search text that they enter into an <input> box, and we’ll use isSearching to tell the directive when a search is in progress.

angular.module("MyApp", []).
  directive('mySearchBox', function() {
    return {
      restrict: 'E',
      replace: true,
      scope: {
        searchText: '=',
        isSearching: '='
      },
      template: '<span>My Custom Search Box</span>'
    };
  });

Did you see the scope = {...} property above? That defines the public elements of this directive’s scope. Notice how we used '='? That tells Angular that we want a scope variable to have two way binding to the outside world. This is part of the directive’s HTML interface. This allows callers to use our directive like this:

<my-search-box
  search-text="someScopeVariable"
  is-searching="someOtherScopeVariable">
</my-search-box>

In other words, the directive allows outside callers to bind their own scope variables to this directive’s searchText and isSearching variables. This is how you communicate with the directive from the outside world.

The variables someScopeVariable and someOtherScopeVariable come from the enclosing scope, which usually is managed by a controller outside the directive. If, for example, we had <div ng-controller="MyController"> enclosing <my-search-box>, then someScopeVariable and someOtherScopeVariable would be managed by MyController.

In the code above, the search-text gets normalized into searchText in the directive’s scope. Likewise, is-searching becomes isSearching.

When we assign search-text="someScopeVariable", we are telling Angular to bind this directive’s searchText scope variable to a scope variable from the enclosing scope called someScopeVariable. Any time the enclosing scope’s someScopeVariable changes, the directive’s scope variable searchText will also change. And it works in the other direction too. Any time the directive changes its searchText variable, Angular will automatically change the enclosing scope’s someScopeVariable to match.

These scope variables are useless unless we also make them visible somehow, so let’s modify our template HTML to use them. While we’re at it, let’s make this actually look like a search box instead of a simple piece of text:

angular.module("MyApp", []).
  directive('mySearchBox', function() {
    return {
      restrict: 'E',
      scope: {
        searchText: '=',
        isSearching: '='
      },
      controller: function($scope) {
        $scope.localSearchText = '';
        $scope.clearSearch = function() {
          $scope.searchText = "";
          $scope.localSearchText = "";
        };
        $scope.doSearch = function() {
          $scope.searchText = $scope.localSearchText;
        };
      },
      replace: true,
      template:
      '<form>' +
        '<div>' +
          '<input ng-model="localSearchText" type="text" />' +
        '</div>' +
        '<div>' +
          '<button ng-click="clearSearch()" class="btn btn-small">Clear</button>' +
          '<button ng-click="doSearch()"    class="btn btn-small">Search</button>' +
        '</div> ' +
        '<div ng-show="isSearching">' +
          '<img ng-show="isSearching" src="http://loadinggif.com/images/image-selection/3.gif" /> ' +
          'Searching...' +
        '</div>' +
      '</form>'
    };
  })

Remember, I suggest you use templateUrl instead of template when your HTML starts to grow like this.

Now we have a search box form that lets the user enter some search terms, which we store in the directive’s scope as localSearchText. Notice that we didn’t put localSearchText in the scope: {…} definition, because it needs no external binding. In other words, this is a “private” scope variable that the directive uses to store the human text input until we are ready to actually do the search. This is because we don’t want every single keystroke to initiate a search. Only when the user clicks the “Search” button.

Notice also that we added a controller with controller: function($scope) {...}. This code defines a typical controller so our directive can take action in response to user input, like button clicks with ng-click.

This HTML also provides a “Clear” button to zero out the search box and a spinner to show when the search is in progress. The directive relies on outside callers to tell it when a search is in progress by setting the isSearching variable to a “truthy” value.

After making these changes to app.js, our page should look like this:

Now that directive is finished. Notice it doesn’t do much. It requires an outsider to stimulate it into action.

Step 3: Creating a Controller

Let’s create a controller that communicates with the directive we wrote. This isn’t strictly required to allow two directives to communicate, but in most cases it will be necessary because someone has to glue them together with the business logic specific to your application. This is a job well suited to an Angular controller.

Let’s make a simulated city search that allows the user to search for cities. Because it’s an example, the search will always return the same results.

<div ng-controller="CitySearchController">
  <h1>Search for Cities</h1> 
  <my-search-box search-text="citySearchText" is-searching="isSearchingForCities"></my-search-box>
</div>

And the accompanying controller, which I placed in app.js:

function CitySearchController($scope, $timeout) {
  $scope.$watch("citySearchText", function(citySearchText) {
    $scope.citySearchResults = [];
    if (citySearchText) {
      $scope.isSearchingForCities = true;
      $timeout(function() {
        // simulated search that always gives the same results
        $scope.isSearchingForCities = false;
        $scope.citySearchResults = ['New York', 'London', 'Paris', 'Moab'];
      }, 1000);
    } else {
      $scope.isSearchingForCities = false;
    }
  });
}

This controller’s $scope.citySearchText and $scope.isSearchingForCities variables are bound to the directive’s scope variables searchText and isSearching because of the HTML attributes that we specified on <my-search-box>.

This controller is pretty basic, so I won’t describe every line of code. All it does is $watch() for changes to citySearchText and get some fake search results with Angular’s $timeout service.

Because of Angular’s excellent isolation-centric design, this controller would be very easy to write unit tests for, but that’s another article.

When you visit the page, you should now see a search box. If you enter some text into the input box and click “Search”, you’ll see a spinner for 1 second, which then disappears:

Step 4: Finally add a second directive

Now we’re ready to add another directive. For this example, we’ll create a search results directive. This directive has almost the same public interface (i.e., scope) as the <my-search-box> directive, with one extra variable: the actual search results that caller wants to display.

It looks like this:

 directive('mySearchResults', function() {
    return {
      restrict: 'E',
      transclude: true,
      scope: {
        isSearching: '=',
        searchResults: '=',
        searchText: '='
      },
      replace: true,
      template:
        '<div ng-hide="isSearching">' +
          '<h4 ng-show="searchResults">Found {{searchResults.length}} Search Results For "{{searchText}}":</h4>' +
          '<ul ng-show="searchResults">' +
            '<li ng-repeat="searchResult in searchResults">' +
              '{{searchResult}}' +
            '</li>' +
          '</ul>' +
        '</div>'
    };
  });

Everything should look familiar from your study of the first directive. There are no new concepts here. Let’s use our directive in our HTML:

  <div ng-controller="CitySearchController" style="margin: 20px">
      <h1>Search for Cities</h1> 
      <my-search-box search-text="citySearchText" is-searching="isSearchingForCities"></my-search-box>
      <my-search-results is-searching="isSearchingForCities" search-results="citySearchResults" search-text="citySearchText"></my-search-results>
    </div>

Now your page should show some search results after clicking the “Search” button and waiting for 1 second:

Just to prove that there is indeed zero coupling between the two directives and the controller, I added a second controller and a second instance of each directive to search for fruits, which you can see in the plunk below.

The Finished Product

Here it is: http://plnkr.co/edit/6KxHY8ZpE83Z2PeNWlYi?p=preview

Conclusion

From the sheer length of this article, you may think that this is complicated, but the reality is that it only took me about 30 minutes to whip together the working example in the plunk.

Main points I hope you took from this article:

  • Directives are easy to create.
  • Directives have their own scope
  • Directives can communicate with the outside world via HTML attributes
  • Directives encourage loose coupling
  • Directives encourage reuse and UI consistency
  • Directives are easy to test (well, this is a future article)
  • Controllers can use directives with zero coupling
  • The Prime Directive has nothing to do with AngularJS (bummer)

13 Responses to “Communication Between Directives in AngularJS”

  1. Mike Murray Says:

    It took me a while to set aside the time to read this, but I’m glad I did. I wanted to see a simple (yet not trivial) example of what AngularJS can do. I like how Angular allows you to better control and nest scope than KnockoutJS.

    The other criticism of Knockout I have is that it doesn’t discourage bad bindings in your HTML; you can find many examples of people putting complicated and unholy logic in their HTML attributes just because you can and Knockout will eval it. I like how Angular seems to encourage you to place the logic in the JavaScript where it belongs (in a defined module even) exposed via properties and methods, thus keeping the bindings in your markup very simple and clean.

    My view engine of choice in .NET is an open source view engine called Spark. It actually has had a feature for a while now that is exactly like directives. I love how you can create reusable UI controls and reference them as HTML nodes that flow very cleanly with the rest of the markup. The idea of this concept in your JavaScript is very welcome in my book.

    I’ve always believed frameworks like Angular and Knockout could help produce very clean separation between markup and behavior, taking testability to a new level. I’d love to see your followup post on this. Thanks for a great post!

  2. Dave Says:

    Thanks for the thoughtful commentary Mike. I feel obligated to point out that Angular is not as righteous as you may suppose in its limitation of binding expressions. It does indeed allow you to sin in this regard. The bound expressions can contain a small mountain of unholy logic, like so:

    ng-show=”foo && bar && !isSomething()”

    It’s easy (well, possible) to get carried away, but it’s equally easy to wrap that expression up into a testable controller function and just call that instead:

    ng-show=”shouldShowMe()”

    I was surprised to learn that Spark had also implemented a directive-like feature. I really thought the AngularJS guys were being novel with this approach. Oh well, at least they got it. :)

    Directives are really cool, but I would still use and love AngularJS without them. Their Controller/Template paradigm is genius.

  3. angel Says:

    man this article is truly amazing..I wish you had written the angularjs documentation :D ….would be cool more articles about directives because is the hard part about angular…maybe something related to compile function?? :D

  4. Jeremy Says:

    Thanks for this; it was well-written and very helpful.

  5. Sai Gudigundla Says:

    Dave,

    Excellent article. Thanks a lot for putting it together. It clearly explains the the concept of Directives with great examples. Looking forward for your feature posts.

  6. Jason Says:

    This is an excellent AngularJS tutorial, easily the best I’ve come across yet. Thanks so much!

  7. Freewind Says:

    Thanks for the great example, it’s very useful.

    And I have a small question about the `scope` of directive. You said:

    > Each instance of a directive has its own scope.

    But from the [document of angular](http://docs.angularjs.org/guide/directive), it seems the only a directive with `scope:true` or `scope: { … }` has it’s own scope, otherwise it doesn’t (by default). You can see this live demo:

    Please correct me if I misunderstand anything, I’m learning angularjs now.

  8. Dave Says:

    Freewind: you are 100% correct.

  9. Jon Says:

    Nicely explained, and I agree with angel, this article should be in the Angular documentation which gets waaay to technical too quickly

    thanks a lot

  10. Abhishek Says:

    Well documented. Would be good provide more articles about directives.

  11. Lance Says:

    Thanks! This helped me complete the directive puzzle. I’ve been writing angular apps for a little while, and I’ve struggled with how to write directives in a best-practice fashion.

  12. Nate Says:

    Great article, a good job showing how the directives are used. I imagine your transclude option up there is an artifact of an earlier version you were working with.

  13. kapil Says:

    I have been reading/studying books, blogs about ng directive controllers,scope etc. for past week and by far this article is the best explanation I have found so far. Thanks for this great post.
    I’ve a question from what I have gathered so far from my study:

    In your second directory ‘mySearchResults’ above, if we add – require: ‘^mySearchBox’, then we wouldn’t need isSearching and searhText variables in the scope of ‘mySearchResults’, because then these variables will be available from mySearchResult’s controller – is my this understanding correct?

Leave a Reply