Inside The AngularJS Directive Compiler

These are the annotated and extended slides and code snippets from a talk I've given at ng-vegas 2015, ng-nl 2015, and JavaScript Frameworks Days 2015.

Angular’s directive system is one of its defining features, and for many of us, one of the reasons we started using Angular in the first place. Being able to extend HTML to your needs is a superpower that Angular gives us.

The problem is that the directive system is wrapped inside a very difficult API. Most people struggle with it while learning Angular, and most people never truly learn all of it.

This is a problem, because when you’re using an API, you want to feel in control - that you know how to use it effectively, and that there aren’t any tricks you’re missing.

Of course, the Angular team acknowledges that the API is a problem, and they’re working towards fixing it for Angular 2. It will have a set of simpler, streamlined APIs for making Components and Directives.

But, for the time being, the Directive Definition Object API is what we have.

So if the API isn’t self-explanatory, maybe diving into the source code will help? After all, that’s the beauty of open source: You can just pop the hood and see what’s inside.

With Angular’s directive system, all the code is actually in this one file, called compile.js. Everything there is to know about how directives work is in that file. So let’s take a look.

This is not an easy piece of code to approach. It currently has about 1300 lines of dense JavaScript code in close to a 100 functions, some of them nested on several levels.

It also packs a lot of features in those 1300 lines. In addition to the core directive compilation process, there’s code related to DOM attribute management, controllers, templates, isolate scopes - you name it. If you’re trying to learn how a particular feature works, it’s very difficult to find it and figure it out.

Some of the members of the Angular team have also expressed having difficulty with this code.

To more effectively learn what’s going on with the directive compiler, let’s flip things around a bit. Instead of trying to figure it out by studying the existing source code, let’s write our own little version of the compiler, from fist principles, using nothing but the core JavaScript language and some LoDash. In the process we can build up a mental model of how it works.

We’ll concentrate on four of the core features of the compiler: The DOM compilation process, the linking of directives to scopes, the management of scope inheritance, and the management of isolate scopes and bindings.

Let’s begin by talking about DOM compilation.

What we mean by compilation in Angular is this process, implemented by the $compile service, that has inputs and outputs.

The inputs to this process are a collection of directives that you have registered, and a piece of DOM that you want to apply those directives to.

What the compiler does is combine those two inputs: It applies the directives to the DOM. The result - the output of the compiler - is a compiled DOM.

What this looks like in code is you have this thing called the $compileProvider, into which you can register directives (or you can do it indirectly by registering them to a module).

Each directive has a name - such as “myClass”, and a factory function. That factory function returns the directive definition object. This example directive has just one configuration option: The compile method.

In its compile method, a directive can do whatever it needs to do to “apply itself”: Transform or decorate the element, or add behaviour to it. In this example we simply add a CSS class to the element.

What you also need is a piece of DOM you want to apply the directives to. This is the “view” of your application.

To initiate compilation, you can grab the root of that DOM from the page, wrap it inside an angular.element wrapper, and give that to the $compile service. This will then initiate the compilation, by applying any directives that have been registered into the $compileProvider.

What you then end up with is a mutated DOM, which is the result of all those directives having applied themselves to it.

So how can we implement something like this? First of all, we’re going to need a $compileProvider object, so we’ll make a constructor for it.

As we saw, the $compileProvider has a method called “directive”, using which you can register directives. The method takes two arguments: A directive name and its factory function.

function $CompileProvider() {

  this.directive = function(name, factory) {

  };

}

The method stores these directives into an object we have inside the $compileProvider. Its keys are directive names, and its values are factories.

In Angular, directive names are not unique, so we should support having several directive factories for a given name. That means we actually need to make the values in this object arrays of directive factories.

  function $CompileProvider() {

    var directives = {};

    this.directive = function(name, factory) {
      if (!directives.hasOwnProperty(name)) {
        directives[name] = [factory];
      } else {
        directives[name].push(factory);
      }
    };

  }
  

Directive factories also support dependency injection, so we need to plug that in here as well. To help us with that, we need the $provide service.

When the first directive for a given name is registered, we register a regular Angular factory to the application using $provide. The factory’s name is the directive name, followed by the string “Directive”. So when you register “foo”, a “fooFactory” will be registered to the app.

  function $CompileProvider($provide) {

    var directives = {};

    this.directive = function(name, factory) {
      if (!directives.hasOwnProperty(name)) {
        directives[name] = [factory];
        $provide.factory(
          name + 'Directive',
          function() {

          }
        );
      } else {
        directives[name].push(factory);
      }
    };

  }
  

The factory is used to instantiate the directives for that name. When it’s called, it looks up the factories for that name from the directives object, and then invokes each of those factories. The invocations are done with dependency injection support, which is provided by the $injector.invoke method.

  function $CompileProvider($provide) {

    var directives = {};

    this.directive = function(name, factory) {
      if (!directives.hasOwnProperty(name)) {
        directives[name] = [factory];
        $provide.factory(
          name + 'Directive',
          function($injector) {
            var factories = directives[name];
            return factories.map($injector.invoke);
          }
        );
      } else {
        directives[name].push(factory);
      }
    };

  }
  

Now that we have directive registration support, we can move on to actually making the compiler. We’re writing a regular Angular provider, and like any provider, it has a $get method. That method produces the $compile service, whose job it will be to do the actual compilation. This is the function you get when you inject $compile into your application.

  function $CompileProvider($provide) {

    var directives = {};

    this.directive = function(name, factory) {
      if (!directives.hasOwnProperty(name)) {
        directives[name] = [factory];
        $provide.factory(
          name + 'Directive',
          function($injector) {
            var factories = directives[name];
            return factories.map($injector.invoke);
          }
        );
      } else {
        directives[name].push(factory);
      }
    };

    this.$get = function() {

      return function $compile(element) {

      };

    };

  }
  

Now we can start talking about compilation. The very first thing $compile actually does is delegate all the work to a helper function called “compileNode”. This function is where most of the interesting stuff will happen. Its job is to compile a node, as the name implies.

  this.$get = function() {

    function compileNode(element) {

    }

    return function $compile(element) {
      compileNode(element);
    };

  };
  

The first step in compiling a node is to find all the directives that apply to that node. For that, we’ll introduce another helper function called “collectDirectives”. It should return an array of all the directives that match this element.

  this.$get = function() {

    function collectDirectives(element) {

    }

    function compileNode(element) {
      var directives = collectDirectives(element);
    }

    return function $compile(element) {
      compileNode(element);
    };

  };
  

This matching can be done in three different ways: By the element’s tag name, by the element’s attribute names, or by the element’s CSS class names. We try each of these three strategies and return a combined result.

  this.$get = function() {

    function collectDirectives(element) {
      return
        collectElementDirectives(element)
        .concat(collectAttributeDirectives(element))
        .concat(collectClassDirectives(element));
    }

    function compileNode(element) {
      var directives = collectDirectives(element);
    }

    return function $compile(element) {
      compileNode(element);
    };

  };
  

To match directives to an element by the tag name, we first need to look up the tag name. We can do that by reading the standard DOM nodeName attribute of the element. That gives us an uppercase, hyphen-separated string, whereas directive names are in camelCase, so we’ll use LoDash to make a camelCased version of it. We also make a version with the “Directive” suffix so we can access the directive factory.

  function collectElementDirectives(element) {
    var elName = element[0].nodeName;
    var directiveName = _.camelCase(elName);
    var fullDirName = directiveName + 'Directive';
  }
  

What we then do is check from the “directives” object we have in this provider if it has a key matching the camelCased element name. If it does, we use the $injector to instantiate the matching directives. This will invoke the factory we created earlier, and return an array of the directive objects by this name.

  function collectElementDirectives(element) {
    var elName = element[0].nodeName;
    var directiveName = _.camelCase(elName);
    var fullDirName = directiveName + 'Directive';
    if (directives.hasOwnProperty(directiveName)) {
      return $injector.get(fullDirName);
    } else {
      return [];
    }
  }
  

To match directives by attribute names, we do something similar. Since there may be any number of attributes on an element, we just need to loop over them, and return a combined result. For each attribute, we just camelCase the name and do the matching just like we did with the tag name.

  function collectAttributeDirectives(element) {
    var result = [];
    _.forEach(element[0].attributes, function(attr) {
      var directiveName = _.camelCase(attr.name);
      if (directives.hasOwnProperty(directiveName)) {
        var matches = $injector.get(fullDirName);
        result = result.concat(matches);
      }
    });
    return result;
  }
  

Thirdly, to match directives by CSS class names, we repeat the same pattern we used with attributes, but this time with the DOM classList attribute, which gives us a collection of the class names present on this element.

  function collectClassDirectives(element) {
    var result = [];
    _.forEach(element[0].classList, function(cName) {
      var directiveName = _.camelCase(cName);
      if (directives.hasOwnProperty(directiveName)) {
        var matches = $injector.get(fullDirName);
        result = result.concat(matches);
      }
    });
    return result;
  }
  

And now we have a collection of all the directives that match this element. All that’s left to be done is to apply these directives to the element. We can do this by simply looping over the directives and calling the “compile” method of each of them. This is where the directives get to do whatever it is they do.

  function compileNode(element) {
    var directives = collectDirectives(element);
    directives.forEach(function(directive) {
      directive.compile(element);
    });
  }
  

At this point, we can consider this node to be compiled.

Before we’re done with compilation though, there’s one more thing: If you think of it, when you give an element to $compile, you don’ t expect it to just compile that single element, but also any descendants it may have. That means we need to compile the child nodes too.

That’s actually easily accomplished: We can simply loop over the children of the element, and recursively invoke “compileNode” for each of them. So, for each child node, the process of finding directives and then applying them starts anew.

  function compileNode(element) {
    var directives = collectDirectives(element);
    directives.forEach(function(directive) {
      directive.compile(element);
    });
    _.forEach(element.children(), compileNode);
  }
  

What we end up with is a depth-first tree traversal of the DOM tree. For each node, we find directives, apply them, and move on to the first child, and so on. When we’re done traversing the tree, we have compiled the DOM.

What we have so far is kind of useful by itself, though it’s not much more than a kind of a jQuery replacement: A system with which you can match some JavaScript code to be applied to some DOM elements.

Where we start to bring Angular more into the picture is when we start to talk about linking, and that’s because linking brings in Scopes.

Scopes are a really important piece of Angular (well, Angular 1.x, at least). Scopes are where you put your application data and functions so that you can share them between directives, controllers, and views. Scopes are the host of the Angular change detection system, because that’s where you $watch, $digest, and $apply. Scopes also bring in the event system, with $emit, $broadcast, and $on.

So scopes are really important to Angular applications. The process of combining Scopes and directives is called linking.

Let’s extend our picture of what the compiler is a bit. The compiler actually has a second output, in a addition to the compiled DOM. That is the linker for a particular compilation.

The linker is another one of these processes that has inputs and outputs: The linker’s inputs are the compiled DOM (from the previous step), and a Scope object. It combines those two to produce a linked DOM.

What’s a little tricky about this is that there is no $link service in Angular, that you could inject. Instead, the linker is always an output of the compiler. We’ll see how that works in a moment.

We can now have directives with link functions. Here’s an example of a link function, that takes a Scope and an element as arguments, and does something with those two. In the example we simply add a CSS class to the element like we did before, but this time the class name is actually dynamically defined on the scope, as a kind of input argument to the directive.

The most low level way you can define a directive link function is to have it as the return value of the directive’s compile function, as we have here.

How you actually do the linking is that when you call $compile, you hold on to its return value. That is going to be the link function - or the “linker” - for this compilation. You can later call it with a Scope argument, and it will then do the linking for the same DOM that you originally gave to $compile.

To implement what we have just seen, we first of all now need to return something from $compile. That something is the link function. In $compile we simply add a return statement, and in compileNode we actually create that link function. We’ll call it the “node link function”, since its job is to link a node. It takes a single argument, which is a Scope object.

  function compileNode(element) {
    var directives = collectDirectives(element);
    directives.forEach(function(directive) {
      directive.compile(element);
    });
    _.forEach(element.children(), compileNode);

    return function nodeLinkFn(scope) {

    };
  }

  return function $compile(element) {
    return compileNode(element);
  }
  

Linking a node basically just means calling all the link functions of the directives that were matched to the node. Before we can call them, we need to collect them, and that we can do during compilation.

As we go over the matched directives and call their “compile” methods, we should grab the return values of those invocations, because each one will return the link function of that directive. So we get those return values and collect them into an array.

  function compileNode(element) {
    var directives = collectDirectives(element),
        linkFns = [];
    directives.forEach(function(directive) {
      var linkFn = directive.compile(element);
      linkFns.push(linkFn);
    });
    _.forEach(element.children(), compileNode);

    return function nodeLinkFn(scope) {

    };
  }

  return function $compile(element) {
    return compileNode(element);
  }
  

Then, once we get to the linking phase, we can call those collected link functions. We can do that by simply looping over the linkFns array and invoking each function in it. So this is where the individual directive link functions get called.

  function compileNode(element) {
    var directives = collectDirectives(element),
        linkFns = [];
    directives.forEach(function(directive) {
      var linkFn = directive.compile(element);
      linkFns.push(linkFn);
    });
    _.forEach(element.children(), compileNode);

    return function nodeLinkFn(scope) {
      linkFns.forEach(function(linkFn) {
        linkFn(scope, element);
      })
    };
  }

  return function $compile(element) {
    return compileNode(element);
  }
  

So, in the time between compilation and linking, the link functions are waiting in the nodeLinkFn’s function closure, as a kind of internal memory of what exactly should be linked when it finally happens.

Before we’re done linking, we again need to discuss descendant nodes. We should also link them. The key to that is to see how we compiled the child nodes, by recursively invoking “compileNode” for each of them, and how “compileNode” now returns a node link function. By holding on to the return values of the recursive “compileNode” invocations, we end up with an array of node link functions - one for each of the child nodes.

  function compileNode(element) {
    var directives = collectDirectives(element),
        linkFns = [];
    directives.forEach(function(directive) {
      var linkFn = directive.compile(element);
      linkFns.push(linkFn);
    });
    var childLinkFns =
      _.map(element.children(), compileNode);

    return function nodeLinkFn(scope) {
      linkFns.forEach(function(linkFn) {
        linkFn(scope, element);
      })
    };
  }

  return function $compile(element) {
    return compileNode(element);
  }
  

Now we can also call those child link functions once linking occurs. This is essentially just the nodeLinkFn recursively calling another instance of itself.

  function compileNode(element) {
    var directives = collectDirectives(element),
        linkFns = [];
    directives.forEach(function(directive) {
      var linkFn = directive.compile(element);
      linkFns.push(linkFn);
    });
    var childLinkFns =
      _.map(element.children(), compileNode);

    return function nodeLinkFn(scope) {
      childLinkFns.forEach(function(childLinkFn) {
        childLinkFn(scope);
      })
      linkFns.forEach(function(linkFn) {
        linkFn(scope, element);
      })
    };
  }

  return function $compile(element) {
    return compileNode(element);
  }
  

Here we actually invoke the child link functions before invoking the link functions of the element itself. That is because what we have here are so-called “post-link” functions. The definition of post-link is that it is called after child nodes have been linked. There are also pre-link functions, that get called before child node linkage, but post-link is the default and that is what we have here.

Our current implementation always requires a directive to have a compile function that returns a link function. In real-world apps, it is much more common to not have compile function at all, but simply define a link function directly in the directive definition object.

We can support this pattern by modifying our directive factory. As it instantiates each directive with $injector.invoke, it can check if the resulting directive definition object has a link function but does not have compile function. If this is the case, it can create a simple compile function on the fly, which does nothing but return the link function.

  this.directive = function(name, factory) {
    if (!directives.hasOwnProperty(name)) {
      directives[name] = [factory];
      $provide.factory(
        name + 'Directive',
        function($injector) {
          var factories = directives[name];
          return factories.map(function(factory) {
            var directive = $injector.invoke(factory);
            if (directive.link && !directive.compile) {
              directive.compile = function() {
                return directive.link;
              };
            }
            return directive;
          });
        }
      );
    } else {
      directives[name].push(factory);
    }
  };
  

So, a directive always has a compile function, whether you define one or not.

We now have a workable linker, but there is one major problem with it: When you give a Scope to a link function, that same Scope is actually used for absolutely everything - all directives in all elements. There is just a single global scope in the whole application.

In reality, this is almost never what actually happens in Angular apps. Instead, you have a hierarchy of Scopes, whose root is the $rootScope. The hierarchy may have any number of descendant Scopes that come and go during the lifetime of the application.

Furthermore, this Scope hierarchy actually usually mirrors your DOM hierarchy. That’s because you use directives in the DOM that cause new Scopes to be created. ngController is one such directive. You end up with these two similarly (though usually not identically) shaped trees: The DOM tree and the Scope tree.

How this happens is that you can have directives that request new Scopes to be created. They can do this by providing a “scope” attribute with a truthy value.

What then happens is when the directive is linked, it is linked to a Scope that is no longer the parent Scope. It is one that inherits from the parent scope, especially created for this element.

That means the directive can put attributes on the Scope, and be relatively certain it’s not going to clobber any attribute its parent or sibling elements may be accessing. It gains us a bit more modularity.

To implement inherited scopes, we first need to check, during compilation, if we see any directives that want to have new scopes. As we go over the directives in “compileNode”, after we’ve compiled the directive, we can check if it has a truthy value in the scope attribute. If it does, we put it in a tracking variable we can use later during linking.

  function compileNode(element) {
    var directives = collectDirectives(element),
        linkFns = [],
        newScopeDirective;
    directives.forEach(function(directive) {
      var linkFn = directive.compile(element);
      linkFns.push(linkFn);
      if (directive.scope) {
        newScopeDirective = directive;
      }
    });
    var childLinkFns =
      _.map(element.children(), compileNode);

    return function nodeLinkFn(scope) {
      childLinkFns.forEach(function(childLinkFn) {
        childLinkFn(scope);
      })
      linkFns.forEach(function(linkFn) {
        linkFn(scope, element);
      })
    };
  }

  return function $compile(element) {
    return compileNode(element);
  }
  

We only allow one directive that requests a new Scope on each element. If there are two or more, we throw an exception during compilation.

  function compileNode(element) {
    var directives = collectDirectives(element),
        linkFns = [],
        newScopeDirective;
    directives.forEach(function(directive) {
      var linkFn = directive.compile(element);
      linkFns.push(linkFn);
      if (directive.scope) {
        if (newScopeDirective) {
          throw 'More than one directive asking for a new scope'
        }
        newScopeDirective = directive;
      }
    });
    var childLinkFns =
      _.map(element.children(), compileNode);

    return function nodeLinkFn(scope) {
      childLinkFns.forEach(function(childLinkFn) {
        childLinkFn(scope);
      })
      linkFns.forEach(function(linkFn) {
        linkFn(scope, element);
      })
    };
  }

  return function $compile(element) {
    return compileNode(element);
  }
  

We can now use this information during linking: If there was a directive on this element that requested a new Scope, we’ll make a new Scope. Then we use that new Scope for everything: For all directives on this element, and for all child elements. It essentially replaces the parent scope for this DOM subtree.

  function compileNode(element) {
    var directives = collectDirectives(element),
        linkFns = [],
        newScopeDirective;
    directives.forEach(function(directive) {
      var linkFn = directive.compile(element);
      linkFns.push(linkFn);
      if (directive.scope) {
        if (newScopeDirective) {
          throw 'More than one directive asking for a new scope'
        }
        newScopeDirective = directive;
      }
    });
    var childLinkFns =
      _.map(element.children(), compileNode);

    return function nodeLinkFn(scope) {
      if (newScopeDirective) {
        scope = scope.$new();
      }
      childLinkFns.forEach(function(childLinkFn) {
        childLinkFn(scope);
      })
      linkFns.forEach(function(linkFn) {
        linkFn(scope, element);
      })
    };
  }

  return function $compile(element) {
    return compileNode(element);
  }
  

This is how you end up with the parallel hierarchies of the DOM tree and the Scope tree.

So, the way inherited Scopes get created is actually quite simple. Let’s move on to something a bit more involved - isolate Scopes.

An isolate scope is in many ways just like a regular Scope: It is part of the Scope tree and has a parent in it. It participates in digests, and it propagates events up and down.

How it differs from regular inherited Scopes, however, is that it doesn’t share any data with its parent. If there’s an attribute on a parent Scope, an isolated child Scope won’t have it. That’s because with isolate Scopes we skip the creation of the JavaScript prototypal inheritance chain that we have with regular inherited Scopes.

What you can then do with isolate Scopes is create isolate bindings, with which you can say “I want this expression to be evaluated on the parent Scope, and have the value bound to the isolate Scope by this name”. It is kind of like passing arguments to isolate Scopes, instead of sharing all state with them via inheritance.

What causes isolate Scopes to be created are directives that request them. A directive may have a scope attribute, whose value is an object (instead of a boolean). That means it wants to have an isolate scope.

The object may be empty, but it may also have contents. If it has contents, those are the specifications of the bindings that should be set up for that scope.

The values in the object should match with attribute names in the HTML element where the directive is used.

The keys in the object line up with keys that actually end up on the Scope that the directive gets linked to.

The net result is that you can have expressions, defined in the HTML where the directive is used, whose values are bound to attributes on the isolate scope. Again, it is kind of like passing arguments. It’s saying, “I want this expression’s value to be passed to this directive”.

Let’s implement this. We need a second tracking variable in “compileNode,” for tracking any isolate scope directive that we see. As we go over the directives and encounter a truthy scope attribute, we should check if the value of the attribute is an object or not. If it’s an object, it’s an isolate scope directive, and otherwise it’s an inherited scope directive.

  function compileNode(element) {
    var directives = collectDirectives(element),
        linkFns = [],
        newScopeDirective,
        newIsoScopeDirective;
    directives.forEach(function(directive) {
      var linkFn = directive.compile(element);
      linkFns.push(linkFn);
      if (directive.scope) {
        if (newScopeDirective) {
          throw 'More than one directive asking for a new scope'
        }
        if (_.isObject(directive.scope)) {
          newIsoScopeDirective = directive;
        } else {
          newScopeDirective = directive;
        }
      }
    });
    var childLinkFns =
      _.map(element.children(), compileNode);

    return function nodeLinkFn(scope) {
      if (newScopeDirective) {
        scope = scope.$new();
      }
      childLinkFns.forEach(function(childLinkFn) {
        childLinkFn(scope);
      })
      linkFns.forEach(function(linkFn) {
        linkFn(scope, element);
      })
    };
  }

  return function $compile(element) {
    return compileNode(element);
  }
  

We still only allow one such directive per each element: You can either have an inherited scope directive or an isolate scope directive on an element, but not both.

  function compileNode(element) {
    var directives = collectDirectives(element),
        linkFns = [],
        newScopeDirective,
        newIsoScopeDirective;
    directives.forEach(function(directive) {
      var linkFn = directive.compile(element);
      linkFns.push(linkFn);
      if (directive.scope) {
        if (newScopeDirective || newIsoScopeDirective) {
          throw 'More than one directive asking for a new/isolated scope'
        }
        if (_.isObject(directive.scope)) {
          newIsoScopeDirective = directive;
        } else {
          newScopeDirective = directive;
        }
      }
    });
    var childLinkFns =
      _.map(element.children(), compileNode);

    return function nodeLinkFn(scope) {
      if (newScopeDirective) {
        scope = scope.$new();
      }
      childLinkFns.forEach(function(childLinkFn) {
        childLinkFn(scope);
      })
      linkFns.forEach(function(linkFn) {
        linkFn(scope, element);
      })
    };
  }

  return function $compile(element) {
    return compileNode(element);
  }
  

We can now use this information in the node link function. If there was an isolate scope directive, we’ll make an isolate scope. We can do this by again calling the $new method on the Scope we were given, but this time we’ll pass it the argument “true”, which means it should make an isolated child instead of a regular one.

  function compileNode(element) {
    var directives = collectDirectives(element),
        linkFns = [],
        newScopeDirective,
        newIsoScopeDirective;
    directives.forEach(function(directive) {
      var linkFn = directive.compile(element);
      linkFns.push(linkFn);
      if (directive.scope) {
        if (newScopeDirective || newIsoScopeDirective) {
          throw 'More than one directive asking for a new/isolated scope'
        }
        if (_.isObject(directive.scope)) {
          newIsoScopeDirective = directive;
        } else {
          newScopeDirective = directive;
        }
      }
    });
    var childLinkFns =
      _.map(element.children(), compileNode);

    return function nodeLinkFn(scope) {
      var isoScope;
      if (newScopeDirective) {
        scope = scope.$new();
      }
      if (newIsoScopeDirective) {
        isoScope = scope.$new(true);
      }
      childLinkFns.forEach(function(childLinkFn) {
        childLinkFn(scope);
      })
      linkFns.forEach(function(linkFn) {
        linkFn(scope, element);
      })
    };
  }

  return function $compile(element) {
    return compileNode(element);
  }
  

What we don’t do here is replace the parent scope with the isolate scope. Instead, we create a new variable “isoScope” for it. That’s because we should not use this isolate Scope for everything, like we did with inherited Scopes. We should use it for one thing only, and that is to link the directive that asked for the isolate scope. If there are any other directives present on the element, they should know nothing about the isolate Scope. The same is true for child elements.

So, we need to know which link function should be given the isolate scope.

In order to do this, we need to do a bit more bookkeeping. As we get these link functions from the directive compile functions, we’ll put in a reference to each one to the directive that generated that link function. This is just for our internal bookkeeping purposes.

  directives.forEach(function(directive) {
    var linkFn = directive.compile(element);
    linkFn.directive = directive;
    linkFns.push(linkFn);
    if (directive.scope) {
      if (newScopeDirective || newIsoScopeDirective) {
        throw 'More than one directive asking for a new/isolated scope'
      }
      if (_.isObject(directive.scope)) {
        newIsoScopeDirective = directive;
      } else {
        newScopeDirective = directive;
      }
    }
  });
  

We can then check that attribute when we call the link functions. We can test if the link function’s directive is the isolate scope directive. If it is, only then do we link it to the isolate scope. Otherwise we link it to the surrounding scope. That is how we target the isolate scope to be used for this one directive only.

  return function nodeLinkFn(scope) {
    var isoScope;
    if (newScopeDirective) {
      scope = scope.$new();
    }
    if (newIsoScopeDirective) {
      isoScope = scope.$new(true);
    }
    childLinkFns.forEach(function(childLinkFn) {
      childLinkFn(scope);
    })
    linkFns.forEach(function(linkFn) {
      var isIso = (linkFn.directive === newIsoScopeDirective);
      linkFn(isIso ? isoScope : scope, element);
    })
  };
  

That pretty much takes care of the creation of isolate scopes, but they are at this point still completely empty. That’s because we don’t support the isolate bindings yet. So let’s go ahead and add support for them too.

We’ll set up the isolate bindings in the node link function, right after we have created the isolate Scope. We loop over each specification in the Scope object, and inside the loop set up the binding.

  return function nodeLinkFn(scope) {
    var isoScope;
    if (newScopeDirective) {
      scope = scope.$new();
    }
    if (newIsoScopeDirective) {
      isoScope = scope.$new(true);
      _.forEach(
        newIsoScopeDirective,
        function(spec, scopeName) {

        }
      );
    }
    childLinkFns.forEach(function(childLinkFn) {
      childLinkFn(scope);
    })
    linkFns.forEach(function(linkFn) {
      var isIso = (linkFn.directive === newIsoScopeDirective);
      linkFn(isIso ? isoScope : scope, element);
    })
  };
  

The values in the scope specs were these strings that begin with the = sign, followed by a DOM attribute name. The first thing we do is extract that DOM attribute name from the string using a regex.

  return function nodeLinkFn(scope) {
    var isoScope;
    if (newScopeDirective) {
      scope = scope.$new();
    }
    if (newIsoScopeDirective) {
      isoScope = scope.$new(true);
      _.forEach(
        newIsoScopeDirective,
        function(spec, scopeName) {
          var attrName = spec.match(/^=(.*)/)[1];
        }
      );
    }
    childLinkFns.forEach(function(childLinkFn) {
      childLinkFn(scope);
    })
    linkFns.forEach(function(linkFn) {
      var isIso = (linkFn.directive === newIsoScopeDirective);
      linkFn(isIso ? isoScope : scope, element);
    })
  };
  

This will be a camelCased attribute name, whereas in the DOM we use hyphen-separated strings, so we need to denormalize the attribute name, which we can do with the _.kebabCase function from LoDash.

  return function nodeLinkFn(scope) {
    var isoScope;
    if (newScopeDirective) {
      scope = scope.$new();
    }
    if (newIsoScopeDirective) {
      isoScope = scope.$new(true);
      _.forEach(
        newIsoScopeDirective,
        function(spec, scopeName) {
          var attrName = spec.match(/^=(.*)/)[1];
          var denormalized = _.kebabCase(attrName);
        }
      );
    }
    childLinkFns.forEach(function(childLinkFn) {
      childLinkFn(scope);
    })
    linkFns.forEach(function(linkFn) {
      var isIso = (linkFn.directive === newIsoScopeDirective);
      linkFn(isIso ? isoScope : scope, element);
    })
  };
  

We can then look up the corresponding attribute value from the element that we are linking. That gives us the expression we should evaluate, that was defined by the directive user.

  return function nodeLinkFn(scope) {
    var isoScope;
    if (newScopeDirective) {
      scope = scope.$new();
    }
    if (newIsoScopeDirective) {
      isoScope = scope.$new(true);
      _.forEach(
        newIsoScopeDirective,
        function(spec, scopeName) {
          var attrName = spec.match(/^=(.*)/)[1];
          var denormalized = _.kebabCase(attrName);
          var expr = element.attr(denormalized);
        }
      );
    }
    childLinkFns.forEach(function(childLinkFn) {
      childLinkFn(scope);
    })
    linkFns.forEach(function(linkFn) {
      var isIso = (linkFn.directive === newIsoScopeDirective);
      linkFn(isIso ? isoScope : scope, element);
    })
  };
  

Our job is to evaluate this expression on the parent scope, bind the value to the isolate scope, and keep it in sync over time. For that purpose, we can use $watch: We can simply watch the expression on the parent scope, and have a listener function that sets the attribute on the isolate scope. The attribute’s name is defined in the binding specification.

  return function nodeLinkFn(scope) {
    var isoScope;
    if (newScopeDirective) {
      scope = scope.$new();
    }
    if (newIsoScopeDirective) {
      isoScope = scope.$new(true);
      _.forEach(
        newIsoScopeDirective,
        function(spec, scopeName) {
          var attrName = spec.match(/^=(.*)/)[1];
          var denormalized = _.kebabCase(attrName);
          var expr = element.attr(denormalized);
          scope.$watch(
            expr,
            function(newValue) {
              isoScope[scopeName] = newValue;
            }
          );
        }
      );
    }
    childLinkFns.forEach(function(childLinkFn) {
      childLinkFn(scope);
    })
    linkFns.forEach(function(linkFn) {
      var isIso = (linkFn.directive === newIsoScopeDirective);
      linkFn(isIso ? isoScope : scope, element);
    })
  };
  

This kind of takes care of the binding, but there’s one more thing, which is that these “=binding” bindings are actually so-called two-way bindings. That means the data should flow in two directions: Not only should we watch the expression on the parent scope, but we should also be prepared to watch the attribute on the isolate scope, and propagate the change up to the parent scope if it changes. This is what two-way data binding means.

To support two-way data binding, we need to refactor things a bit first. We’ll need to parse that expression string we have into an expression function, which we can do with Angular’s $parse service. We inject $parse and then use it to produce an expression function from the expression string. It is a regular function we can call, that will evaluate the expression on a given Scope.

  this.$get = function($injector, $parse) {
    // ...
  };
  
    return function nodeLinkFn(scope) {
      var isoScope;
      if (newScopeDirective) {
        scope = scope.$new();
      }
      if (newIsoScopeDirective) {
        isoScope = scope.$new(true);
        _.forEach(
          newIsoScopeDirective,
          function(spec, scopeName) {
            var attrName = spec.match(/^=(.*)/)[1];
            var denormalized = _.kebabCase(attrName);
            var expr = element.attr(denormalized);
            var exprFn = $parse(expr);
            scope.$watch(
              expr,
              function(newValue) {
                isoScope[scopeName] = newValue;
              }
            );
          }
        );
      }
      childLinkFns.forEach(function(childLinkFn) {
        childLinkFn(scope);
      })
      linkFns.forEach(function(linkFn) {
        var isIso = (linkFn.directive === newIsoScopeDirective);
        linkFn(isIso ? isoScope : scope, element);
      })
    };
  

Then we’ll make an alternative implementation of the watcher we have. It’s a special kind of watcher that doesn’t actually have any listener function attached to it. It’s just a standalone watch function. It’s basically like a callback to the digest cycle: A function Angular will call for us whenever a digest occurs. Inside it we’ll do our own change detection.

    return function nodeLinkFn(scope) {
      var isoScope;
      if (newScopeDirective) {
        scope = scope.$new();
      }
      if (newIsoScopeDirective) {
        isoScope = scope.$new(true);
        _.forEach(
          newIsoScopeDirective,
          function(spec, scopeName) {
            var attrName = spec.match(/^=(.*)/)[1];
            var denormalized = _.kebabCase(attrName);
            var expr = element.attr(denormalized);
            var exprFn = $parse(expr);
            scope.$watch(function() {

            });
          }
        );
      }
      childLinkFns.forEach(function(childLinkFn) {
        childLinkFn(scope);
      })
      linkFns.forEach(function(linkFn) {
        var isIso = (linkFn.directive === newIsoScopeDirective);
        linkFn(isIso ? isoScope : scope, element);
      })
    };
  

We first evaluate the expression function on the parent Scope, to get the current value of the expression. Then we get the current value of the attribute on the isolate Scope. Then we compare those two. If they’re different, there’s been a change and we need to sync the new value to the isolate scope.

    return function nodeLinkFn(scope) {
      var isoScope;
      if (newScopeDirective) {
        scope = scope.$new();
      }
      if (newIsoScopeDirective) {
        isoScope = scope.$new(true);
        _.forEach(
          newIsoScopeDirective,
          function(spec, scopeName) {
            var attrName = spec.match(/^=(.*)/)[1];
            var denormalized = _.kebabCase(attrName);
            var expr = element.attr(denormalized);
            var exprFn = $parse(expr);
            scope.$watch(function() {
              var newParentValue = exprFn(scope);
              var childValue = isoScope[scopeName];
              if (newParentValue !== childValue) {
                isoScope[scopeName] = newParentValue;
              }
            });
          }
        );
      }
      childLinkFns.forEach(function(childLinkFn) {
        childLinkFn(scope);
      })
      linkFns.forEach(function(linkFn) {
        var isIso = (linkFn.directive === newIsoScopeDirective);
        linkFn(isIso ? isoScope : scope, element);
      })
    };
  

What we end up with is a watcher that’s functionally equivalent to the previous one. The new one is just a bit more low-level, since we do our own change detection in it instead of letting Angular do it.

Now we can build the two-way data binding on top of this. The key to that is to introduce one more tracking value, called “parentValue”. This variable will always hold the latest value we have seen the parent expression to have. We remember between digests, what the last known value of that expression was.

    return function nodeLinkFn(scope) {
      var isoScope;
      if (newScopeDirective) {
        scope = scope.$new();
      }
      if (newIsoScopeDirective) {
        isoScope = scope.$new(true);
        _.forEach(
          newIsoScopeDirective,
          function(spec, scopeName) {
            var attrName = spec.match(/^=(.*)/)[1];
            var denormalized = _.kebabCase(attrName);
            var expr = element.attr(denormalized);
            var exprFn = $parse(expr);
            var parentValue;
            scope.$watch(function() {
              var newParentValue = exprFn(scope);
              var childValue = isoScope[scopeName];
              if (newParentValue !== childValue) {
                isoScope[scopeName] = newParentValue;
              }
              parentValue = newParentValue;
            });
          }
        );
      }
      childLinkFns.forEach(function(childLinkFn) {
        childLinkFn(scope);
      })
      linkFns.forEach(function(linkFn) {
        var isIso = (linkFn.directive === newIsoScopeDirective);
        linkFn(isIso ? isoScope : scope, element);
      })
    };
  

As we then detect a change, we can check if the new value of the parent expression differs from the last one we saw. If it does, the parent expression’s value has changed and we need to do what we did before, which is to sync the value to the isolate scope.

    return function nodeLinkFn(scope) {
      var isoScope;
      if (newScopeDirective) {
        scope = scope.$new();
      }
      if (newIsoScopeDirective) {
        isoScope = scope.$new(true);
        _.forEach(
          newIsoScopeDirective,
          function(spec, scopeName) {
            var attrName = spec.match(/^=(.*)/)[1];
            var denormalized = _.kebabCase(attrName);
            var expr = element.attr(denormalized);
            var exprFn = $parse(expr);
            var parentValue;
            scope.$watch(function() {
              var newParentValue = exprFn(scope);
              var childValue = isoScope[scopeName];
              if (newParentValue !== childValue) {
                if (newParentValue !== parentValue) {
                  isoScope[scopeName] = newParentValue;
                }
              }
              parentValue = newParentValue;
            });
          }
        );
      }
      childLinkFns.forEach(function(childLinkFn) {
        childLinkFn(scope);
      })
      linkFns.forEach(function(linkFn) {
        var isIso = (linkFn.directive === newIsoScopeDirective);
        linkFn(isIso ? isoScope : scope, element);
      })
    };
  

But if the new and old parent values are the same, the parent expression’s value hasn’t changed. Since we still have detected a change, that means the isolate scope attribute value must have changed, and here’s where we need to have the second, upward direction of the two-way data binding.

    return function nodeLinkFn(scope) {
      var isoScope;
      if (newScopeDirective) {
        scope = scope.$new();
      }
      if (newIsoScopeDirective) {
        isoScope = scope.$new(true);
        _.forEach(
          newIsoScopeDirective,
          function(spec, scopeName) {
            var attrName = spec.match(/^=(.*)/)[1];
            var denormalized = _.kebabCase(attrName);
            var expr = element.attr(denormalized);
            var exprFn = $parse(expr);
            var parentValue;
            scope.$watch(function() {
              var newParentValue = exprFn(scope);
              var childValue = isoScope[scopeName];
              if (newParentValue !== childValue) {
                if (newParentValue !== parentValue) {
                  isoScope[scopeName] = newParentValue;
                } else {

                }
              }
              parentValue = newParentValue;
            });
          }
        );
      }
      childLinkFns.forEach(function(childLinkFn) {
        childLinkFn(scope);
      })
      linkFns.forEach(function(linkFn) {
        var isIso = (linkFn.directive === newIsoScopeDirective);
        linkFn(isIso ? isoScope : scope, element);
      })
    };
  

The key to this is to use a special method that parsed Angular expressions have, called “assign”. This is a method that evaluates an expression in a special way.

If you have an expression like “foo.x”, and you evaluate that as a normal expression, what you get is the value of “scope.foo.x”. But you can also call assign on it, and pass in a new value. That will cause “scope.foo.x” to be reassigned to the new value. This is something the Angular expression parser provides us: It figures out how to reassign the value of an expression.

This is a method we can use to send the isolate scope attribute’s value to the parent scope. It will go and replace it there. And that is how the upward change propagation happens.

    return function nodeLinkFn(scope) {
      var isoScope;
      if (newScopeDirective) {
        scope = scope.$new();
      }
      if (newIsoScopeDirective) {
        isoScope = scope.$new(true);
        _.forEach(
          newIsoScopeDirective,
          function(spec, scopeName) {
            var attrName = spec.match(/^=(.*)/)[1];
            var denormalized = _.kebabCase(attrName);
            var expr = element.attr(denormalized);
            var exprFn = $parse(expr);
            var parentValue;
            scope.$watch(function() {
              var newParentValue = exprFn(scope);
              var childValue = isoScope[scopeName];
              if (newParentValue !== childValue) {
                if (newParentValue !== parentValue) {
                  isoScope[scopeName] = newParentValue;
                } else {
                  exprFn.assign(scope, childValue);
                }
              }
              parentValue = newParentValue;
            });
          }
        );
      }
      childLinkFns.forEach(function(childLinkFn) {
        childLinkFn(scope);
      })
      linkFns.forEach(function(linkFn) {
        var isIso = (linkFn.directive === newIsoScopeDirective);
        linkFn(isIso ? isoScope : scope, element);
      })
    };
  

Finally, all we need to do is make sure our tracking variables are correctly set up for the next digest: That the parent value we are tracking matches the one we just replaced it with.

    return function nodeLinkFn(scope) {
      var isoScope;
      if (newScopeDirective) {
        scope = scope.$new();
      }
      if (newIsoScopeDirective) {
        isoScope = scope.$new(true);
        _.forEach(
          newIsoScopeDirective,
          function(spec, scopeName) {
            var attrName = spec.match(/^=(.*)/)[1];
            var denormalized = _.kebabCase(attrName);
            var expr = element.attr(denormalized);
            var exprFn = $parse(expr);
            var parentValue;
            scope.$watch(function() {
              var newParentValue = exprFn(scope);
              var childValue = isoScope[scopeName];
              if (newParentValue !== childValue) {
                if (newParentValue !== parentValue) {
                  isoScope[scopeName] = newParentValue;
                } else {
                  exprFn.assign(scope, childValue);
                  newParentValue = childValue;
                }
              }
              parentValue = newParentValue;
            });
          }
        );
      }
      childLinkFns.forEach(function(childLinkFn) {
        childLinkFn(scope);
      })
      linkFns.forEach(function(linkFn) {
        var isIso = (linkFn.directive === newIsoScopeDirective);
        linkFn(isIso ? isoScope : scope, element);
      })
    };
  

And this hopefully gives you a better idea of what exactly Angular does when it goes to work on your DOM: How directives are matched and applied to elements, how directive link functions get called, how new inherited and isolated scopes are created in the process, and how isolate bindings work. This also hopefully helps make a little bit more sense out of the directive API.

If you want to learn about the implementation of AngularJS in great detail, this is what my book Build Your Own AngularJS is about. In its close to 1000 pages we build up a version of AngularJS from first principles using test-driven development.

The book covers Scopes, expressions, dependency injection, Promises, $http, and all the features of $compile. It is currently in early access and will be finalised soon. You can get it from my website, where there is also a sample chapter available.

Tero Parviainen is an independent software consultant with a background in Java, Ruby, and Clojure. However, he's mostly been doing front-end work for the past couple of years.

Tero is the two-time organizer of the Clojure Cup programming competition, and the author of Build Your Own AngularJS.

Tero blogs at and tweets as @teropa.