Dependency injection (DI) is one of the core design patterns in angular and angular applications. DI allows you to replace almost any part of angular framework or angular application with a custom implementation, allowing for a highly flexible, maintainable and testable code-base.
Dependency injection is a very common pattern in Java and other statically typed languages. While undervalued among JavaScript developers, we feel strongly that DI in JavaScript allows us to achieve the same benefits as in other languages.
This document will focus on using dependency injection in angular. It is outside of the scope of this document to explain details of dependency injection. For more information on this topic, please refer to these links:
Angular's dependency injection story begins with a service
. Service in angular lingo is a
JavaScript object, function, or value that is created by angular's injector via a provided factory
function. The factory function is registered with angular via angular.service
.
// register a factory for a uniqueId service. angular.service('uniqueId', function(){ // calling the factory function creates the instance function var id = 0; return function(){ // calling the counter instance function will return and increment the count return ++id; } });
At run-time we can access the uniqueId
service by looking it up with the service locator like
this:
// create new root scope which has the injector function `$service()` var scope = angular.scope(); // use the `$service` function to look up the service instance function var idGenerator = scope.$service('uniqueId'); expect(idGenerator()).toBe(1); // subsequent lookups using the same root scope return the service instance var idGenerator2 = scope.$service('uniqueId'); expect(idGenerator).toBe(idGenerator2); // since it is same instance calling idGenerator2 returns 2; expect(idGenerator2()).toBe(2);
The service
registry seems like a lot of work, so what are the benefits? To
answer this question, it’s important to realize that in large scale applications there are a lot of
services which are often dependent on each other, as in this example:
angular.service('gadgetFactory', function(uniqueId){ return function(){ return {gadgetId: uniqueId()}; }; }, {$inject: ['uniqueId']});
Specifically, notice that the gadgetFactory
takes uniqueId
service in its arguments. It also
declares this dependency with the $inject
property. There are several benefits to this approach:
main
method for an application responsible for instantiating and wiring
these services. The order of service instantiation and wiring can be inferred by examining the
$inject
annotations.angular.service.$xhr
.)More importantly, as we'll soon learn, controllers and other components of angular applications can also declare their dependencies on services and these will be provided without explicitly looking them up, but let's not get ahead of ourselves.
Lastly, it is important to realize that all angular services are singletons – application singletons to be more precise. This means that there is only one instance of a given service per injector. And since angular is lethally allergic to the global state, it's absolutely possible to create multiple injectors each with its own instance of a given service (but that is not typically needed, except in tests where this property is crucially important).
The injector
is responsible for resolving the service dependencies in the
application. It gets created and configured with the creation of a root scope in your application.
The injector is responsible for caching the instances of services, but this cache is bound to the
scope. This means that different root scopes will have different instances of the injector. While
typical angular applications will only have one root scope (and hence the services will act like
application singletons), in tests it is important to not share singletons across test invocations
for isolation reasons. We get this isolation by having each test create its own separate root scope.
// crate a root scope var rootScope = angular.scope(); // accesss the service locator var myService = rootScope.$service('myService');
So far we have been talking about injector as a service locator. This is because we have been
explicitly calling the $service
method to gain access to the service. Service locator is not
dependency injection since the caller is still responsible for retrieving the dependencies. True
dependency injection is like Chuck Norris. Chuck does not ask for dependencies; he declares them.
The most common place to use dependency injection in angular applications is in
controllers
. Here’s a simple example:
function MyController($route){ // configure the route service $route.when(...); } MyController.$inject = ['$route'];
In this example, the MyController
constructor function takes one argument, the
$route
service. Angular is then responsible for supplying the
instance of $route
to the controller when the constructor is instantiated. There are two ways to
cause controller instantiation – by configuring routes with the $route service or by referencing the
controller from the HTML template, such as:
<!doctype html> <html xmlns:ng="http://angularjs.org" ng:controller="MyController"> <script src="http://code.angularjs.org/angular.min.js" ng:autobind></script> <body> ... </body> </html>
When angular is instantiating your controller, it needs to know what services, if any, should be
injected (passed in as arguments) into the controller. Since there is no reflection in JavaScript,
we have to supply this information to angular in the form of an additional property on the
controller constructor function called $inject
. Think of it as annotations for JavaScript.
MyController.$inject = ['$route'];
The information in $inject
is then used by the injector
to call the
function with the correct arguments.
At times you’ll need to use dependency injection pragmatically, usually when instantiating controllers manually or writing unit tests. This section explains how to go about it.
The simplest form of dependency injection is manual retrieval of scopes, known as service locator.
We say manual because we are asking the injector for an instance of the service (rather then having
the injector provide them to the function). This should be rare since most of the time the dependent
services should be injected into the controller using the $inject
property array.
// create a root scope. The root scope will automatically have // `$service` method defined which is configured with all services. // Each instance of root scope will have separate instances of services. var rootScope = angular.scope(); // ask for a service explicitly var $window = rootScope.$service('$window');
In a typical angular application the dependency injection is most commonly used when creating controllers.
// declare our own service by registering a factory function. angular.service('counter', function(){ var count = 0; return function(){ return count++; }; }); // example of a controller which depends on '$window' and 'counter' service // notice that there is an extra unbound parameter 'name' which will not // be injected and must be supplied by the caller. function MyController($window, counter, name) { } // we must declare the dependencies explicitly and in the same order as in // the constructor function. This information is used by the dependency // injection to supply the arguments. // Notice the lack of 'name' argument which makes it an unbound argument. MyController.$inject = ['$window', 'counter']; // Create a root scope which creates the the injector var rootScope = angular.scope(); // use the '$new()' method instead of standard 'new' keyword operator to // create an instance of MyController and have the dependency injection // supply the arguments to the controller. The dependency injection only // supplies the bound arguments in `$inject` all addition arguments are // curried from the '$new', in our case 'Alexandria' is the argument which // will be curried to the 'name' argument, while '$window' and 'counter' // are supplied by the dependency injection. var myController = rootScope.$new(MyController, 'Alexandria'); // NOTE: the returning controller will be a child scope of parent scope, // in this case the root scope.
NOTE: this section is quite lame. The concept it is trying to describe is more closely related to scope#new than scope#$service. We need a better example to discuss here. Ideally a parent controller creating a child controller imperatively via $new where the child controller's constructor function declares a portion of its dependencies via $inject property, but another portion is supplied by the caller of $new (e.g. parentCtrl.$new(ChildCtrl, configParam1, configParam2);
Finally, you may need to call functions but have the $inject
properties of the function be
supplied by the injector.
// create a root scope with the `$service` injector. var rootScope = angular.scope(); // given a function such as function greet ($window, name) { $window.alert(this.salutation + ' ' + name); } greet.$inject = ['$window']; // you can call function 'greet' such that the injector supplies the // '$window' and the caller supplies the function 'this' and the 'name' // argument. var fnThis = {salutation: 'Hello'} rootScope.$service(greet, fnThis, 'world');
$inject
EXPERIMENTAL: this is an experimental feature, see the important note at the end of this section for drawbacks.
We resort to $inject
and our own annotation because there is no way in JavaScript to get a list of
arguments. Or is there? It turns out that calling .toString()
on a function returns the function
declaration along with the argument names as shown below:
function myFn(a,b){} expect(myFn.toString()).toEqual('function myFn(a,b){}');
This means that angular can infer the function names after all and use that information to generate
the $inject
annotation automatically. Therefore the following two function definitions are
equivalent:
// given a user defined service angular.service('serviceA', ...); // inject '$window', 'serviceA', curry 'name'; function fnA($window, serviceA, name){}; fnA.$inject = ['$window', 'serviceA']; // inject '$window', 'serviceA', curry 'name'; function fnB($window, serviceA_, name){}; // implies: fnB.$inject = ['$window', 'serviceA'];
If angular does not find an $inject
annotation on the function, then it calls the .toString()
and tries to infer what should be injected using the following rules:
$
is angular service and will be added to $inject
property array._
will be added to the $inject
property array but we strip the _
$
nor _
, must not have $
nor _
(these are free arguments for curring)IMPORTANT
Minifiers/obfuscators change the names of function arguments and will therefore break the $inject
inference. For this reason, either explicitly declare the $inject
or do not use
minifiers/obfuscators. In the future, we may provide a pre-processor which will scan the source code
and insert the $inject
into the source code so that it can be minified/obfuscated.