Wednesday, September 5, 2012

AngularJS - Dependency Injection, Modules, and Services

I want to start by saying that AngularJS' Dependency Injection (DI) engine really is a little piece of genius that I plan on using in every js app that I write.  I'm hoping that some day they spin it out as a project of it's own.  That said, the relationship between Services, Modules, and DI took me some time to get used to.  For better or for worse, there are about three or four ways to get to the same place, and for beginners it's not clear which you should use.  Below I've chosen a single way, and have described why I haven't used the others.

UPDATES 12/13/12 -

  • Fixed some inaccuracies now that I'm a bit more angular-savvy!

In the scenario below we have a single controller  That has a dependency on a service (DateDisplayerService).  That service, in turn, has a dependency on another service (CurrentDateService).

So here's the dependency chain:

Ctrl depends on DateDisplayerService depends on CurrentDateService.

We describe how these are defined, instantiated, and wired together.

The examples are written in coffeescript.  Those unfamiliar with coffeescript can use js2coffee.org
to convert to js.  A working example of this code is available on plunker.

Services

First let's create some services.  A 'service' is a very loaded term these days, but here it means a class that handles a subset of my app's functionality.  A service might need to depend on other services to get its job done.  Those dependencies will, in this case, be provided to the service via it's constructor.
# has a dependency on a date service
class DateDisplayerService

   constructor: (@dateService) ->
   display: ->
      "The date is #{@dateService.date()}.  Thanks!"

# A date service
class CurrentDateService

   date: ->
      new Date()
No AngularJS here.  Just creating two plain ol' classes, where the displayer service depends on a date service. Nice and isolated, so easily testable / replaceable.  Note that we're not instantiating anything - that's left to the Dependency Injection (DI) engine.

Registering service factories with a module

Modules are a somewhat arbitrary way of grouping your 'stuff', 'stuff' being controllers, services, directives, filters, and most importantly, your app.  How you group them is largely a matter of convention, but the AngularJS docs recommend that every type of 'thing' have its own module.  You can also check out this, which has a very opinionated module philosophy (the one that I follow in practice).

Since every service needs to be wrapped in an AngularJS module, below we create a services module for all of our services and stick them in there.
# Define the module
myServiceModule =
  angular.module(
    'myApp.services'        # every module needs a unique name
    []                      # this module has no dependencies on other modules
)
Next we add a factory for each of our services, first the date service.
myServiceModule.factory(
   'dateService'    # services need unique names as well
   ()->             # function that defines how to get an instance of the service
      return new CurrentDateService
)
AngularJS will call the service factory function defined above when it needs to create an instance of dateService. It will call it once, since the date service will be a singleton.  From then on it will used the already-created instance.

The name of the service is very important!  AngularJS will keep a map of all of your services, using this name to look them up. If you have a lot of services, you'll want to ensure that you've got a good naming convention here.

Note that the docs say you can also use $provide to register your services. Generally you should only have to do this if you're creating your own Providers or during testing, which is a whole other topic.  For beginners, I suggest you avoid $provide.

Now the fun part (pay attention!): creating a factory for a service that has a dependency.
myServiceModule.factory(
   'dateDisplayerService'            
   [                     # The all-important factory array!
      'dateService'      # The *name* of the dependency that we'll need
      (dateSrv)->        # all dependencies are passed as arguments
         return new DateDisplayerService(dateSrv) 
   ]
)
When  AngularJS needs an instance of 'dateDisplayerService', it first gets instances of all of it's dependencies (in this case, just dateService). It then passes those dependencies to the factory function, where you can create your instance of DateDisplayerService.

The All-Important Factory Array

The 'all-important factory array' from the last snippet (my name, not AngularJS's) is something you'll use all over the place. It contains the following, in this order:

  1. a list of the names of all of the dependencies
  2. as the last argument, the factory function that will create your instance

Since above we have only one dependency, our factory array has only two variables: the name of the date service we need and our factory function.  Later when we create the controller, this array will have a bit more.  Now this is one way to inject dependencies into your services, called 'array notation' in the docs.  The docs describe two other ways of doing dependency injection. They are:
  1. Using $inject. I use $inject only in tests and when I need to pull a dependency at runtime.
  2. Using 'DI inference'. Doesn't work with minifiers and is a little too much magic for me.

Injecting our services into a controller

Since controller are the only place where data can be exposed to the DOM (via $scope), it's the controllers that need to have access to our services to get the Views what they need.

First thing about creating controllers, FORGET EVERYTHING you see in the documentation.  Do NOT
create a controller as a global function.  Why AngularJS even makes this an option I don't know.

Controllers should instead be defined in exactly the same way as your services - all wrapped nice and 
tightly in a module, as follows:
myControllerModule = angular.module(
   'myApp.controllers',
   ['myApp.services']   # Important! our controller module depends on our services module!
)

myControllerModule.controller(
   'Ctrl'       # Name of the controller
    [           # The factory array again
      '$scope'  # The controller has two dependencies. Angular's '$scope', and ..
      'dateDisplayerService'       # The date displayer service 
      # The function that defines the controller
      (scope, dateDisplayer)->    
         scope.dateDisplay = dateDisplayer.display()
      ]
)
By adding the dateDisplay variable to the $scope, we've exposed what we needed to expose to the View.

Now we just have to create a module for our application, and ensure that the app module depends on the controller module.  We also, since this is a coffeescript app, need to manually bootstrap the application.  In a normal javascript app you'd achieve the same thing using the ng-application attribute.

angular.module('myApp.app', ['myApp.controllers'])
angular.bootstrap(document, ['myApp.app'])