Avoid AngularJS Dependency Annotation with Rails

Greg Lazarev

In AngularJS, it is a common practice to annotate injected dependencies for controllers, services, directives, etc.

For example:

(function() {
  'use strict';

  angular.module('exampleApp', [])
    .directive('myDirective', ['$location', 'myService', myDirective]);

  function myDirective($location, myService) {
    return {
      scope: {},
      link: link,
    };

    function link(scope, element, attrs) {
      myService.doSomething($location.path());
      // ...
    }
  }
})();

Notice how the annotation of the injected arguments causes duplication ($location and $myService appears twice). It becomes the responsibility of the developer to ensure that the actual arguments and the annotation are always in sync. If they are not, problems will occur that can cause lost time while head-scratching. As the list of arguments grows, it gets even harder to maintain.

The reason for needing to annotate the injected dependencies is documented in the AngularJS docs under “Dependency Annotation”:

To allow the minifiers to rename the function parameters and still be able to inject right services, the function needs to be annotated…

JavaScript minifiers will rename function arguments and parameters to something short and ambiguous (usually using just one letter). In that case, AngularJS does not know what service to inject since it tries to match dependencies to the argument names.

ng-annotate and ngannotate-rails

ng-annotate is a tool that automatically adds AngularJS dependency injection annotations. It is non-intrusive and sends annotated results to the minifier. It handles rebuilding annotations, adding and removing them as needed.

ngannotate-rails is a Ruby gem that wraps ng-annotate and seamlessly integrates it into Rails’ asset pipeline. Both, JavaScript and CoffeeScript, are supported. Using it is as easy as adding it to the Gemfile and bundling.

Once ngannotate-rails is in place, AngularJS code can be written without needing to annotate the injected dependencies. The previous code can now be written as:

(function() {
  'use strict';

  angular.module('exampleApp', []).directive('myDirective', myDirective);

  function myDirective($location, myService) {
    return {
      scope: {},
      link: link,
    };

    function link(scope, element, attrs) {
      myService.doSomething($location.path());
      // ...
    }
  }
})();

Now there is no duplication of argument names and no weird array notation.

Manual dependency annotation

95% of the time ngannotate-rails will handle annotating the dependencies, but there are situations where dependencies cannot be derived using static analysis. In such a situation, manual annotation is required.

John Papa’s wonderful community styleguide suggests the following:

Use $inject to manually identify your dependencies for Angular components.

This is recommended because it’s the same technique that ng-annotate uses. In addition, it has the benefit of being a straight-forward assignment, rather than an unusual array notation.

(function() {
  'use strict';

  angular.module('exampleApp', []).directive('myDirective', myDirective);

  function item($location, myService) {
    return {
      scope: {},
      link: link,
      controller: controller,
    };

    function link(scope, element, attrs) {
      myService.doSomething($location.path());
      // ...
    }

    controller.$inject = ["$scope"];
    function controller($scope) {
      $scope.foo = "bar";
      // ...
    }
  }
})();

In this case, myService will be automatically annotated by ngannotate-rails, but $scope that is passed into the controller will not be. Thus, it must be manually annotated via controller.$inject = ["$scope"].

Keep in mind these situations and consider checking for any missing injection annotations next time you get an Unknown Provider error in your application.

What’s next

If you found this useful, you might also enjoy: