Skip to content

Instantly share code, notes, and snippets.

@ttsvetko
Created September 25, 2018 07:02
Show Gist options
  • Save ttsvetko/d5d7681209a777cb3c013bec8eb60705 to your computer and use it in GitHub Desktop.
Save ttsvetko/d5d7681209a777cb3c013bec8eb60705 to your computer and use it in GitHub Desktop.
Angular Dependency Injection

Angular Dependency Injection

@ngdoc overview @name Dependency Injection @description

Dependency Injection (DI) is a software design pattern that deals with how code gets hold of its dependencies.

The Angular injector subsystem is in charge of service instantiation, resolution of dependencies, and provision of dependencies to components as requested.

For in-depth discussion about DI, see Dependency Injectionat Wikipedia, Inversion of Control by Martin Fowler, or read about DI in your favorite software design pattern book.

DI in a nutshell

There are only three ways an object or a function can get a hold of its dependencies:

  1. The dependency can be created, typically using the new operator.

  2. The dependency can be looked up by referring to a global variable.

  3. The dependency can be passed in to where it is needed.

The first two options of creating or looking up dependencies are not optimal because they hard code the dependency. This makes it difficult, if not impossible, to modify the dependencies. This is especially problematic in tests, where it is often desirable to provide mock dependencies for test isolation.

The third option is the most viable, since it removes the responsibility of locating the dependency from the component. The dependency is simply handed to the component.

function SomeClass(greeter) {
  this.greeter = greeter;
}

SomeClass.prototype.doSomething = function(name) {
  this.greeter.greet(name);
}

In the above example SomeClass is not concerned with locating the greeter dependency, it is simply handed the greeter at runtime.

This is desirable, but it puts the responsibility of getting hold of the dependency on the code that constructs SomeClass.

<img class="pull-right" style="padding-left: 3em; padding-bottom: 1em;" src="img/guide/concepts-module-injector.png">

To manage the responsibility of dependency creation,each Angular application has an {@link angular.injector injector}. The injector is a service locator that is responsible for construction and lookup of dependencies.

Here is an example of using the injector service:

// Provide the wiring information in a module
angular.module('myModule', []).

  // Teach the injector how to build a 'greeter'
  // Notice that greeter itself is dependent on '$window'
  factory('greeter', function($window) {
    // This is a factory function, and is responsible for
    // creating the 'greet' service.
    return {
      greet: function(text) {
        $window.alert(text);
      }
    };
  });

// New injector is created from the module.
// (This is usually done automatically by angular bootstrap)
var injector = angular.injector(['myModule', 'ng']);

// Request any dependency from the injector
var greeter = injector.get('greeter');

Asking for dependencies solves the issue of hard coding, but it also means that the injector needs to be passed throughout the application. Passing the injector breaks the Law of Demeter. To remedy this, we turn the dependency lookup responsibility to the injector by declaring the dependencies as in this example:

<!-- Given this HTML -->
<div ng-controller="MyController">
  <button ng-click="sayHello()">Hello</button>
</div>
// And this controller definition
function MyController($scope, greeter) {
  $scope.sayHello = function() {
    greeter.greet('Hello World');
  };
}

// The 'ng-controller' directive does this behind the scenes
injector.instantiate(MyController);

Notice that by having the ng-controller instantiate the class, it can satisfy all of the dependencies of MyController without the controller ever knowing about the injector. This is the best outcome. The application code simply asks for the dependencies it needs, without having to deal with the injector. This setup does not break the Law of Demeter.

Dependency Annotation

How does the injector know what service needs to be injected?

The application developer needs to provide annotation information that the injector uses in order to resolve the dependencies. Throughout Angular, certain API functions are invoked using the injector, as per the API documentation. The injector needs to know what services to inject into the function. Below are three equivalent ways of annotating your code with service name information. These can be used interchangeably as you see fit and are equivalent.

Inferring Dependencies

The simplest way to get hold of the dependencies, is to assume that the function parameter names are the names of the dependencies.

function MyController($scope, greeter) {
  // ...
}

Given a function the injector can infer the names of the service to inject by examining the function declaration and extracting the parameter names. In the above example $scope, and greeter are two services which need to be injected into the function.

While straightforward, this method will not work with JavaScript minifiers/obfuscators as they rename the method parameter names. This makes this way of annotating only useful for [pretotyping](http://www.pretotyping.org/), and demo applications.

$inject Annotation

To allow the minifers to rename the function parameters and still be able to inject right services the function needs to be annotated with the $inject property. The $inject property is an array of service names to inject.

var MyController = function(renamed$scope, renamedGreeter) {
  ...
}
MyController['$inject'] = ['$scope', 'greeter'];

In this scenario the ordering of the values in the '$inject' array must match the ordering of the arguments to inject. Using above code snippet as an example, '$scope' will be injected into 'renamed$scope' and 'greeter' into 'renamedGreeter'. Care must be taken that the $inject annotation is kept in sync with the actual arguments in the function declaration.

This method of annotation is useful for controller declarations since it assigns the annotation information with the function.

Inline Annotation

Sometimes using the $inject annotation style is not convenient such as when annotating directives. For example:

someModule.factory('greeter', function($window) {
  // ...
});

Results in code bloat due to needing a temporary variable:

var greeterFactory = function(renamed$window) {
  // ...
};

greeterFactory.$inject = ['$window'];

someModule.factory('greeter', greeterFactory);

For this reason the third annotation style is provided as well.

someModule.factory('greeter', ['$window', function(renamed$window) {
  // ...
}]);

Keep in mind that all of the annotation styles are equivalent and can be used anywhere in Angular where injection is supported.

Where can I use DI?

DI is pervasive throughout Angular. You can use it in controllers, services, directives, filters, animations, and run and config blocks.

DI in controllers

Controllers are classes which are responsible for application behavior. The recommended way of declaring controllers is using the array notation:

someModule.controller('MyController', ['$scope', 'dep1', 'dep2', function($scope, dep1, dep2) {
  ...
  $scope.aMethod = function() {
    ...
  }
  ...
}]);

This avoids the creation of global functions for controllers and also protects against minification.

Factory methods

Factory methods are responsible for creating most objects in Angular. Examples are directives, services, and filters. The factory methods are registered with the module, and the recommended way of declaring factories is:

angular.module('myModule', []).
  config(['depProvider', function(depProvider){
    ...
  }]).
  factory('serviceId', ['depService', function(depService) {
    ...
  }]).
  directive('directiveName', ['depService', function(depService) {
    ...
  }]).
  filter('filterName', ['depService', function(depService) {
    ...
  }]).
  run(['depService', function(depService) {
    ...
  }]);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment