AngularJS Directives: A Beginner’s Practical Guide

Published Nov 24, 2014Last updated Sep 21, 2017

This post will teach you how to pass in a callback function to the directive, talk about when to use an Angular controller vs link function, and more. This article is a continuation of the post How to Better Understand Directives, which is based on the office hours hosted by AngularJS expert mentor Tero Parviainen, the author of Build Your Own AngularJS and the well-received tutorial on scopes and digest, which inspired a Chinese and Russian translation and much discussion.

The text below is a summary done by the Codementor team and may vary from the original video and if you see any issues, please let us know!


Tip #1: When Scope is Set to False

When a scope is set to false in the link function, it doesn’t create a new child scope. However, this doesn’t mean the link function doesn’t have a scope, since there will always be a scope. In this case, however, the link function will share the same scope as the parent’s, so they will share the same object. For example, if the parent scope is myTitle, you could, from the directive, write something like

$scope.myCtrl.title = 'Evil title';

and change it. When the scope is set to false, you’d have no protection from state changes and no way of stopping them. Personally, I rarely find a reason to use scope: false unless there are multiple directives on the same element, where one element has scope: true and the rest share that one. However, sharing scopes with the parent elements is seldom a good idea.

Tip #2: How to Pass in a Callback Function to the Directive

Say you have some kind of function the directive needs to call from outside to get back some data. I’ve done this before so I know you can send it in with the same kind of data binding I showed you with the = sign, since everything is an object in JavaScript.

There is, however, a third kind of data binding you can use. However, it’s a bit difficult to work with, and it’s called expression binding.

<!DOCTYPE html>
<html>
<body ng-app="myApp" ng-controller="MyController as myCtrl">
  <expandable-section section-title="myCtrl.myTitle"
              section-body="myCtrl.myBody"
              some-expression="myCtrl.someFunction()">
  </expandable-section>
  <script src="//ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js"></script>
  <script src="//ajax.googleapis.com/ajax/libs/angularjs/1.2.19/angular.min.js"></script>
  <script src="app.js"></script>

</body>
</html>

With normal binding, you just pass in the function, however you lose the context (in this case, myCtrl). To retain that context, you’d have to do some special stuff to make it work.

var app = angular.module('myApp', []);
app.controller('MyController', function() {
  this.myTitle = "Some title";
  this.myBody = 'Some body';
  console.log('Hello', this)
});

app.directive('expandableSection', function() {
  return {
    restrict: 'E',
    template: '<section><h2>{{title}}</h2><article>{{body}}</article>'
    replace: true,
  scope: {
    title: '=sectionTitle',
    body: '=sectionBody'
    expr: '&someExpression' //here you can bind the someExpression with the & sign
  },
  link: function($scope, $element, attrs) {
    $scope.expr();
    $element.find('article').hide();
    $element.find('h2', click(function() {
      $element.find('article').toggle();
    });
  }
  };
});

After you do this, the $scope.expr() becomes a function that evaluates the expression myCtrl.someFunction.

However, this is only nice when you don’t have any arguments to pass back. When you have arguments in myCtrl.someFunction(), you’ll notice your expression binding does not quite work anymore. For example, it wouldn’t work with events (If you’ve used ng-click, you’d know there is this magic called “events”).

Let’s give it a try.

<!DOCTYPE html>
<html>
<body ng-app="myApp" ng-controller="MyController as myCtrl">
  <expandable-section section-title="myCtrl.myTitle"
    			      section-body="myCtrl.myBody"
            some-expression="myCtrl.someFunction(magicArgument)">
  </expandable-section>
  <script src="//ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js"></script>
  <script src="//ajax.googleapis.com/ajax/libs/angularjs/1.2.19/angular.min.js"></script>
  <script src="app.js"></script>

</body>
</html>

Where our app.js would look like

var app = angular.module('myApp', []);
app.controller('MyController', function() {
  this.myTitle = "Some title";
  this.myBody = 'Some body';
  console.log('Hello', this)
  this.someFunction = function(magicArg) {
  console.log('hello', magicArg, this);
  }
});

app.directive('expandableSection', function() { 
  return {
    restrict: 'E',
  template: '<section><h2>{{title}}</h2><article>{{body}}</article>'
  replace: true,
  scope: {
    title: '=sectionTitle',
    body: '=sectionBody'
    expr: '&someExpression' //here you can bind the someExpression with the & sign
  },
  link: function($scope, $element, attrs) {
    $scope.expr(42); //let's say we'd like to do 42 for the expression
    $element.find('article').hide();
    $element.find('h2', click(function() {
      $element.find('article').toggle();
    });
  }
  };
});

You’ll notice your code doesn’t work.

Of course, you can fix this by changing expr: '&someExpression' to expr: '=someExpression' and some-expression="myCtrl.someFunction(magicArgument)" to some-expression="myCtrl.someFunction". However, as I said, you’d lose the context of myCtrl this way.

A way several internals of the Angular directive choose to it is to keep the Angular Argument but instead of binding the expression to the scope, it is parsed.

attrs.someExpression 
$scope.expr(42);

We’d get

var app = angular.module('myApp', []);
app.controller('MyController', function() {
  this.myTitle = "Some title";
  this.myBody = 'Some body';
  console.log('Hello', this)
  this.someFunction = function(magicArg) {
    console.log('hello', magicArg, this);
  }
});

app.directive('expandableSection', function($parse) { //this injects Angular's $parse services into the directive 
  return {
  restrict: 'E',
  template: '<section><h2>{{title}}</h2><article>{{body}}</article>'
  replace: true,
  scope: {
    title: '=sectionTitle',
    body: '=sectionBody' //instead of binding someExpression to the scope, we parse it instead
  },
  link: function($scope, $element, attrs) {
    var expr = $parse(attrs.someExpression) 
    expr($scope.$parent, {magicArgument: 42}); //we'd have to parse the expression to the parent scope and pass in the named locals for it, which is {magicArgument: 42}

    $element.find('article').hide();
    $element.find('h2', click(function() {
    $element.find('article').toggle();
    });
  }
  };
});

After doing these things, we’ll get this.

At any rate, I’ve seen this done in the Angular source code, but I don’t particularly think it’s a good idea to do a lot of this in the application code, since it looks like internal stuff that maybe you shouldn’t be doing. However, from the user’s point of view, this is probably the cleanest and most pleasant way to do things, as it hides the complex stuff inside the directive. So, there is a trade-off in using this.

Tip #3: Localization in a Library/Platform built with AngularJS

There isn’t an “elegant” way to do localization, and I’ve heard people say the Angular team kind of purposely left that kind of consideration out of the framework because they think it’s outside of the sope of the framework. A way you can do this is to have a convention where there is a constant with a special name you define in your module, which you will inject to all of your directives. Once you have a constant, it will be feasible everywhere, but it also completely relies on having everyone follow up to that convention.

For example, you can have this

app.constant('myLanguage', 'fi-FI'); //in which fi-FI means Finnish

And that would be injectable in

app.directive(function(myLanguage) {

So, whoever writes this directive needs to agree that myLanguage is the name that we’d use, since currently there is nothing in the AngularJS framework that gives any more help with that.

Another way to do localizations is through a service provider, which is how the Angular Translate App does it. Angular Translate is a great project that solves internationalization problems, and since it will likely handle translations better, just using the Angular Translate App is a good idea if it fits your purpose. If you still want to do localization yourself, you can also look at its API source code and see it has a $translateProvider, which will tell you what language to use and also have a fallback language.

Tip #4: Use $postDigest

Throughout the years I’ve been coding, I’ve learned that many things can be fixed with a set timer. The $postdigest gives you a way to do things later: right after the next digest, before the browser renders the next time. This will help you keep things working when your ordering of things needs some stuff to happen first.

Tip #5: Learn Through Reading Other People’s Git History

AngularJS itself has a good git history, and they really explain in detail what they’re doing in every commit. You can learn a lot from looking at how they do their own directives, how they do ng-click, ng-repeat, and so on, especially since some of the tricks they use aren’t so well-documented. You can also learn from other well-known and widely-used open source projects such as Angular UI, since several team members seem to have a good grasp of Angular UI. Also, I’ve heard their documentation may be even better than Angular’s official one, so it’s definitely worthwhile to study the git history of open source projects.

I often also wonder about what the difference between Angular controllers and link functions are. In the code example used in this office hours, you can just put the $ sign on the attrs and rename the link function to a controller like this:

controller: function($scope, $element, $attrs) {
  var expr = $parse(attrs.someExpression)
  expr($scope.$parent, {magicArgument: 42});

  $element.find('article').hide();
  $element.find('h2', click(function() {
    $element.find('article').toggle();
  });
}

And then you’ll find everything still works fine.

I usually use the link function when we have to share the controller with some other directive in the same element or child. In those cases, I’d often end up using both the link function and the controller, where the link does something and the controller something else.

The differences I can think of is that the controller can reorder arguments on dependencies and injections, while the link function can’t. In addition, controllers are nicer for code organizational purposes, since you can pull the controller out to an external one like this:

app.controller('ExpandableSectionCtrl', function($parse, $scope, $element, $attrs) { //move parse to the controller function
  var expr = $parse(attrs.someExpression)
  expr($scope.$parent, {magicArgument: 42});

  $element.find('article').hide();
  $element.find('h2', click(function() {
  $element.find('article').toggle();
  });
 }
};
});

app.directive('expandableSection', function() {
  return {
    restrict: 'E',
  template: '<section><h2>{{title}}</h2><article>{{body}}</article>'
  replace: true,
  scope: {
    title: '=sectionTitle',
    body: '=sectionBody'
  },
  controller: 'ExpandableSectionCtrl' //here you'll refer to the controller as a string
  };
});

This will be especially nice if you have to unit test the controller and don’t want to test the whole directive. So, at one point, I thought that maybe I should always use controllers because I couldn’t figure out a reason not to. Then, I cam across having to use another controller in my directives, where I’d do a require and have another controller somewhere. I couldn’t figure out how to put the controller function directly into the directive, so I ended up using the link function like this:

app.directive('expandableSection', function($parse) {
  return {
    restrict: 'E',
    template: '<section><h2>{{title}}</h2><article>{{body}}</article>'
    replace: true,
    require: '^someOtherCtrl', //an external controller
  scope: {
    title: '=sectionTitle',
    body: '=sectionBody'
  },
  link: function($scope, $element, attrs, someOtherCtrl) {
    var expr = $parse(attrs.someExpression)
    expr($scope.$parent, {magicArgument: 42});
    $element.find('article').hide();
    $element.find('h2', click(function() {
      $element.find('article').toggle();
    });
  }
  };
});

Currently this is the only API I can think of in the directive where you can’t put the controller in the directly, but this is literally the only reason I can think of to not use the controller.

Furthermore, there is also a stylistic thing about normal Angular controllers where it’s a big no-no to touch the DOM. You’re pretty much forbidden from doing that, but you can still inject elements into the directive controllers. Still, when I do that, I sometimes feel I maybe shouldn’t, and that there should be some distinction where DOM stuff happens in the link function, and other stuff happens in the controller. All in all, as long as you’re consistent in your approach, I think it doesn’t make much of a difference in which one you use.

Discover and read more posts from Tero Parviainen
get started
Enjoy this post?

Leave a like and comment for Tero