In Angular 1, directives are a way for developers to extend HTML. This means introducing new behaviours to the DOM via custom tags or attributes. You can change what a directive matches by using the restrict option. By default it is set to 'EA', meaning it works on elements (tags) and attributes.

These days, I’m restricting directives to only elements (restrict: 'E'). The reason is that attributes are hard to compose. When multiple attribute directives exist on one element, it can be challenging to figure out the resulting behaviour.

Warning: This post was written for AngularJS 1.4 and may contain outdated information. Please see this post for things to watch out for when reading older Angular posts. If you would like me to update the content of this post, please ping me on Twitter at @jay_soo. If there are enough interest, I might make an update.

Composability issues with attributes

Say I have these two directives that changes text colour depending on a value it observes.

m.directive('greenIf', () => ({
  restrict: 'A',
  link(scope, element, attrs) {
    scope.$watch(attrs.greenIf, (value) => {
      if (value) {
        element.css('color', 'green');
      }
    });
  }
}));

m.directive('redIf', () => ({
  restrict: 'A',
  link(scope, element, attrs) {
    scope.$watch(attrs.redIf, (value) => {
      if (value) {
        element.css('color', 'red');
      }
    });
  }
}));

Now if I add them to the same element like so, what result do I expect?

<div red-if="true" green-if="true">
  What colour am I?
</div>

The answer is actually green because it comes after red when sorting alphabetically, so its link function is invoked last. In this case, both directives have the default priority of 0, so according to the official docs, their ordering is undefined. However, looking at the source code shows us that they are indeed sorted alphabetically. Just don’t count on this ordering in the future.

Now, I could guarantee their ordering by setting specific priority values. I don’t like this approach, because directives with a priority value forces developers to have to map out all of the directives’ interactions in their head in order to reason about them. As the number of directives grow, it becomes impossible to load everything into your head!

Composing elements instead of attributes

If I were to re-write the directives on elements instead, the result is much easier to understand.

m.directive('greenIf', () => ({
  restrict: 'E',
  link(scope, element, attrs) {
    scope.$watch(attrs.value, (value) => {
      if (value) {
        element.css('color', 'green');
      }
    });
  }
}));

m.directive('redIf', () => ({
  restrict: 'E',
  link(scope, element, attrs) {
    scope.$watch(attrs.value, (value) => {
      if (value) {
        element.css('color', 'red');
      }
    });
  }
}));

Now, with the following HTML, can you guess what the colour is?

<green-if value="true">
  <red-if value="true">
    What colour am I?
  </red-if>
</green-if>

The answer is of course red!

Hopefully I’ve convinced you, at least a little bit, on why limiting directives to elements makes them much simpler.

In a future post, I will expand more on component patterns and how they can be applied to Angular 1 directives.



blog comments powered by Disqus