Wednesday, July 10, 2013

Angular injection

tl;dr is marked throughout by ∴

I don't like magical code. AngularJS is magical. I must fix this.

Dependency injection was one of AngularJS's first evil magicks I encountered. The idea that calling this function

function myFunction($scope, $http) {
...
}
will magically reach out to the universe and grab the correct values for $scope and $http runs contrary to all the JavaScript I've ever used. You can't do that!

So I dug in to discover the magicks. And now it's not magic! It's great! It's rougly equivalent to import in Python or require in Ruby. Here's how it works:

Modules

AngularJS groups injectable things together into modules. The following code will:

  1. make a module named woods
  2. add a provider to the woods module named Eeyore, which has a constant value
var woods = angular.module('woods', []);
woods.value('Eeyore', 'sad')

Here's some of the source for the module function plus context (see the full source here — the comments are helpful):

// from setupModuleLoader()
function ensure(obj, name, factory) {
return obj[name] || (obj[name] = factory());
}

// ...

var modules = {};
return function module(name, requires, configFn) {
// ...
return ensure(modules, name, function() {
// ...
var moduleInstance = {
// ...
requires: requires,
name: name,
provider: invokeLater('$provide', 'provider'),
factory: invokeLater('$provide', 'factory'),
service: invokeLater('$provide', 'service'),
value: invokeLater('$provide', 'value'),
constant: invokeLater('$provide', 'constant', 'unshift'),
filter: invokeLater('$filterProvider', 'register'),
controller: invokeLater('$controllerProvider', 'register'),
directive: invokeLater('$compileProvider', 'directive'),
// ...
};
// ...
return moduleInstance;
// ...
});
};

  1. The ensure(obj, name, factory) function makes sure that obj has an attribute named name, creating it by calling factory if it doesn't.
  2. The module(name, requires, configFn) function adds a moduleInstance named name to the global-ish modules object (by using ensure).

angular.module(...) adds a module to some global-ish module registry.

Injectors

Injectors find providers from among the modules it knows about. By default, AngularJS creates an injector through the bootstrapping process. We can also make an injector with angular.injector() and use it to access providers within modules:

// Run this in a JavaScript console (on a page that has AngularJS)

// Make a woods module with an Eeyore provider
var woods = angular.module('woods', []);
woods.value('Eeyore', 'sad')

// Make an injector that knows about the 'woods' module.
var injector = angular.injector(['woods'])

// Get poor Eeyore out of the module
injector.get('Eeyore');
// -> "sad"

The creation of injectors and how they know where things are is somewhat recursive (and the code is a little hard to read). I will unravel that magic in another post as it was making this post too long. For now, just know that

Injectors can find the providers you add to modules (e.g. through .value(...) or .factory(...)) and can find modules that were previously added to the global-ish module registry.

Invoke

Using an injector, we can invoke functions with dependency injection:

// Run this in a JavaScript console (on a page that has AngularJS)

// Make a woods module with an Eeyore provider
var woods = angular.module('woods', []);
woods.value('Eeyore', 'sad')

// Make an injector that knows about the 'woods' module.
var injector = angular.injector(['woods'])

// Imbue a function with sadness
function eatEmotion(Eeyore) {
return 'I am ' + Eeyore;
}
injector.invoke(eatEmotion);
// -> "I am sad"

But how does it KNOOooooowwwWWW??

How does AngularJS know the names of the arguments a function is expecting? How does it know that my weather function's arguments is named sunny?

function weather(sunny) {
...
}

That's an internal detail of weather, inaccessible from the outside, no? I've done introspection with Python, but this is JavaScript.

How AngularJS gets the argument names made me laugh out loud when I found it. It's a dirty (effective) trick found in the annontate function (full source):

var FN_ARGS = /^function\s*[^\(]*\(\s*([^\)]*)\)/m;
var FN_ARG_SPLIT = /,/;
var FN_ARG = /^\s*(_?)(\S+?)\1\s*$/;
var STRIP_COMMENTS = /((\/\/.*$)|(\/\*[\s\S]*?\*\/))/mg;
function annotate(fn) {
var $inject,
fnText,
argDecl,
last;

if (typeof fn == 'function') {
if (!($inject = fn.$inject)) {
$inject = [];
fnText = fn.toString().replace(STRIP_COMMENTS, '');
argDecl = fnText.match(FN_ARGS);
forEach(argDecl[1].split(FN_ARG_SPLIT), function(arg){
arg.replace(FN_ARG, function(all, underscore, name){
$inject.push(name);
});
});
fn.$inject = $inject;
}
} else if (isArray(fn)) {
last = fn.length - 1;
assertArgFn(fn[last], 'fn');
$inject = fn.slice(0, last);
} else {
assertArgFn(fn, 'fn', true);
}
return $inject;
}

If you pass a function to annotate it will convert that function to a string and use regular expressions to get the names of the arguments.

I should note, however, that the practice of depending on argument names for injection is discouraged (because of how the names get munged during minification). It makes the code look cleaner, though. Maybe we should work on changing minification to handle this introspective kind of injection.

Which functions have it? Which don't?

When you're just starting with AngularJS, it's a little frustrating that some functions are magic (i.e. are called with injection) and some are seemingly inert. For instance, when writing a directive, link is not called with dependency injection, but controller is.

The provider methods are called with injection (factory, value, etc...). And directive controllers are called with injection. From the official docs:

DI is pervasive throughout Angular. It is typically used in controllers and factory methods.

Sadly, the only way to know if a function is called with dependency injection is to... know. Read the docs or the source, and build up an ample supply of doing it wrong :)

Namespacing

Modules provided to an injector will stomp on each other's providers:

// Run this in a JavaScript console (on a page that has AngularJS)

function mineFor(Thing) {
return "I found " + Thing + "!";
}


// Make two modules that each define a Thing provider
var good_module = angular.module('good', []);
good_module.value('Thing', 'gold');

var bad_module = angular.module('bad', []);
bad_module.value('Thing', 'sour milk');

// Make an injector
var injector = angular.injector(['good', 'bad']);

injector.invoke(mineFor);
// -> "I found sour milk!"

I don't know if this is by design or if there are plans to address it. Be aware of it.

In summary

Dependency injection in AngularJS is roughly equivalent to other languages' including and importing, but scoped to functions. Some of the magic is accomplished by exploiting function.toString() and regular expressions.

Read the official doc about Dependency Injection for some of the motivation behind its use.