Angular 2.0 Dependency Injection - Applied To Backbone TodoMVC

Posted on by Tero Parviainen (@teropa)

Update 2014-05-06: Since this was written, a few changes have been made in the Angular DI library. The relevant changes are highlighted below.

Bits and pieces of AngularJS 2.0 are starting to surface: The design docs are out there for everyone to read, and there are initial implementations available for change detection, logging and profiling, templating, zones, type assertions, and the HTTP client. These are exciting times for an Angular developer!

I like how these components are being developed as independent modules in independent repositories. This makes it possible for app developers to mix and match Angular libraries with other frameworks and tools, whereas Angular 1.x has been much more of an all-or-nothing proposition.

One of the new components I find most interesting is the dependency injection library at angular/di.js. It is the library that will power dependency injection in Angular 2.0, but it is also a standalone DI framework that can be used in non-Angular contexts, enabling some of that mixing and matching.

For an intro to dependency injection in JavaScript and the design goals of the new DI lib, there's not better place to start than Vojta Jina's talk from ng-conf 2014:

I thought I'd take di.js for a spin and plug it into the Backbone variant of the trusty old TodoMVC application. This article describes what happened. You can also see the resulting app on GitHub.

ECMAScript 6 And Beyond

One of the first things that strikes you about di.js and all the other Angular 2.0 libs is that they are written in the next version of JavaScript, ECMAScript 6. This makes the code very different from most of the JavaScript code you see out there: There are classes and methods. There are modules, imports, and exports. There are type hints. There are all kinds of things we haven't seen in JavaScript apps before.

Since the new ES6 features aren't that widely supported by browsers yet, we need tooling to make it all work. Some of the features can be polyfilled, but for many other things we need the Traceur compiler, which takes ES6 source code and outputs ES5 code that can be run in a much wider set of browsers. This is how I expect we'll be running most Angular 2.0 apps early on. It's also very similar to the Dart-to-JS compilation that's needed to run AngularDart apps.

The di.js library actually uses some language features that are not even officially in ES6 - at least not at the moment. The "annotations" such as @Inject are nowhere to be seen in compatibility tables, and in the Traceur compiler they need to be enabled with a special "experimental" flag. I was kind of surprised by this, and I'm not sure what the status of the standardization is. These experimental features are optional and you can use di.js without them. But they do make it a lot nicer.

The Starting Point: ES6 TodoMVC

Since di.js is designed to be used in ES6 applications, we need an ES6 variant of TodoMVC to plug it into. I used Addy Osmani's todomvc-backbone-es6, the Backbone version of TodoMVC rewritten in ES6.

Before digging into di.js, let's look at the way this version of TodoMVC launches: In index.html it first loads its dependencies (jQuery, LoDash, Backbone) using traditional script tags:

<script src="bower_components/jquery/jquery.js"></script>
<script src="bower_components/lodash/dist/lodash.js"></script>
<script src="bower_components/backbone/backbone.js"></script>
<script src="bower_components/backbone.localStorage/backbone.localStorage.js"></script>

Then it loads something called the ES6 module loader. It is a polyfill for the ES6 module loading mechanism that lets us use ES6 code imports and exports in browsers that don't support them natively:

<script src="bower_components/es6-module-loader/dist/es6-module-loader.js"></script>

Finally, the application is loaded using the System.import function:

<script>System.import('js/app')</script>

System.import is part of the ES6 module spec and polyfilled by the ES6 module loader. It finds the file js/app.js relative to the HTML document and loads it in. Additionally, since the code will be ES6 and most browsers don't support that, the module loader runs the code through the Traceur compiler and gives the browser the resulting ES5 code instead.

The actual application code is in two files: todo-app.js contains most of the actual application code, and app.js contains the launching code that taps into jQuery's "document ready" and loads the application.

Setup

It turns out that there's quite a few steps involved in getting things to load correctly. This is probably due to the newness of pretty much all the technologies we're using: di.js, es6-module-loader, Traceur, and ES6 itself. Here's what you need to do:

1. Load di.js

Before we can use dependency injection in the application code, we need to load di.js. TodoMVC uses Bower, but unfortunately di.js isn't available as a Bower package yet. Let's just go old school instead and drop a few files from the di.js repository into the TodoMVC project. _

Update: di.js is actually available via Bower - just specify the Git repository in bower.json:

"di": "https://github.com/angular/di.js.git",

The next step is to get a handle on the Injector class.

Update: Since Bower installs things in its own bower_components directory instead of the root of the webapp - which is where the ES6 module loader looks for them - we'll need to map the actual location to the module loader. For this purpose, we'll need to install systemjs, which is an ES6 module loader library that has the mapping capability. Add the following to bower.json:

"systemjs": "https://github.com/systemjs/systemjs.git",

Then, add the mapping in index.html, just above where the application is loaded:

System.map['di'] = 'bower_components/di/src/index';

Now we can go ahead and load the injector, which is the entry point to the dependency injection framework. It's defined within the DI library, which the loader now knows how to load thanks to our mapping:

import {Injector} from 'di';

2. Load Q

Update: DI no longer uses Q, just native ES6 promises. You can skip this section.

If you try to run this, you will see a 404 because the browser is trying to load a file called `q.js`, which does not exist. The Angular injector uses promises when it needs to load something asynchronously. Currently it does this with [Kris Kowal's Q library](https://github.com/kriskowal/q), and that is why it's trying to load `q.js`. Looks like [the Angular team is looking into more light-weight alternatives for Q](https://docs.google.com/document/d/150lerb1LmNLuau_a_EznPV1I1UHMTbEl61t4hZ7ZpS0/edit?pli=1), but for the time being we'll just need to add it in. Looking at [the di.js source code](https://github.com/angular/di.js/blob/26e3e934ea6c714c05e4f2943391baf2d83c39af/src/injector.js#L6), we can see it's referencing Q like this:
import {resolve} from 'q';
When you load a module using `import`, you do it relative to a base path, which by default is the path of the current page. That means there should be a `q.js` file at the root of the project, in the same directory as `index.html`. So, just grab [a copy of `q.js`](http://cdnjs.cloudflare.com/ajax/libs/q.js/1.0.0/q.js) and put it there.

3. Load es6-shim

Another thing that you run into as soon as you try to use the injector is that it fails with messages like "Map is not defined". The DI framework uses ES6 Maps to store some of its data, and those aren't included in the TodoMVC app, nor are they provided by the Traceur compiler. What we need is a polyfill. There's a good one in the es6-shim project, which we can install with Bower:

bower install es6-shim --save

And then reference in index.html:

<script src="bower_components/es6-shim/es6-shim.js"></script>

4. Set Up Error Logging

This issue has nothing to do with di.js, but fixing it will make your life much easier: By default the TodoMVC application swallows any errors you might have in the application code. This is because there is no error handler set up for the code loading.

What's happening is, the System.import call used in index.html returns a promise that's resolved when the code is loaded, or rejected if the code fails to load. We're currently just throwing that promise away, which means we can't see anything when loading fails.

Here's the change you can make to to get error messages to show:

System.map['di'] = 'bower_components/di/src/index';
System.import('js/app').catch(function(e) {
  console.error(e);
});

5. Enable Experimental Traceur features

The experimental annotation support in Traceur isn't on by default. It is also fairly new, and as it happens, it wasn't there yet in the version of Traceur and es6-module-loader that comes with the current version of the ES6 TodoMVC app.

To fix this one you need to install a newer version of es6-module-loader, by changing the version to 0.5.1 in bower.json and reinstalling. Then, add an explicit script tag for Traceur in index.html (this is something that's needed in es6-module-loader 0.5.x but wasn't in 0.4.x):

<script src="bower_components/es6-module-loader/dist/traceur.js"></script>
<script src="bower_components/es6-module-loader/dist/es6-module-loader.js"></script>

Finally, before loading the application, set the flag for Traceur's experimental features:

traceur.options.experimental = true;
System.map['di'] = 'bower_components/di/src/index';
System.import('js/app').catch(function(e) {
  console.error(e);
});

Setting Up The Injector

With all the dependencies in place, we're ready to start injecting! The first thing to do is to obtain an Injector. We've already imported the class, and now we can instantiate it as the very first thing in the jQuery ready callback in app.js:

$(() => {
  var injector = new Injector();
  new AppView();
  new Filters();
  Backbone.history.start();
});

The Injector is a container that knows how to instantiate application components. We can ask it to instantiate objects and other resources for us, and it'll do so based on the configuration we've provided. The Injector's role is pretty much the same as the role of the $injector service in Angular 1.x. The main difference is that the 2.0 Injector isn't limited to instantiating Angular providers.

We haven't configured anything yet, but we can already use the Injector to instantiate the AppView and the Filters:

$(() => {
  var injector = new Injector();
  injector.get(AppView);
  injector.get(Filters);
  Backbone.history.start();
});

We've basically replaced the new operator with a call to Injector.get, and we're now letting the Injector call new for us indirectly.

When you call Injector.get, you provide the Injector a token as an argument. This token is the identifier of the dependency you want it to instantiate. It can be any JavaScript value, but in this case it is a reference to an ES6 class. The injector will then see if it has any DI configuration related to that token. In this case it won't, so it'll just use the provided token itself to instantiate the dependency. What we get back is an AppView object and a Filters object. Later in this article we'll see cases where the token is not the constructor of the dependency.

The Injector considers all dependencies singletons, so if you call injector.get(AppView) several times, you'll get back the same AppView object each time, from the injector's internal cache. This is essentially the same behavior as the Angular 1.x injector has.

Basic DI: Injecting The Todos Collection

We're instantiating some objects with the Injector, but so far it hasn't bought us very much, if anything. What we should be using the injector for is breaking up manually maintained dependencies and modularizing code. Luckily we have some opportunities for doing just that in todo-app.js.

The first thing we'll "clean up" is the global Todos collection. In todo-app.js there's a very common Backbone pattern: First it defines a TodoList class that extends Backbone's Collection. Then it defines one global instance of that class, which is used to hold all the todo items. We are going to get rid of that global collection and instead inject one where it's needed.

First, remove this line from the file:

var Todos = new TodoList();

Next, we're going to inject an instance of TodoList to the constructors of the classes that need it. AppView is one of them. Its constructor should take a TodoList and assign it to a field:

export class AppView extends View {

  constructor(todos) {
    this.todos = todos;
    // ...
  }

  // ...

}

All the code that references the global Todos variable in AppView should then be updated to reference this.todos instead. For example:

toggleAllComplete() {
  var completed = this.allCheckbox.checked;
  this.todos.each(todo => todo.save({ completed }));
}

The other class that references Todos is Filters. It should also be updated to take todos as a constructor argument and use that collection instance instead of the global one:

export class Filters extends Router {

  constructor(todos) {
    this.todos = todos;
    this.routes = {
      '*filter': 'filter'
    }

    this._bindRoutes();
  }

  filter(param = '') {
    TodoFilter = param;

    this.todos.trigger('filter');
  }
}

Trying to load the application at this point will reveal that we're not quite done yet. We've added a constructor argument to AppView and Filters, and we're also instantiating both of those objects with the Injector, but we haven't yet told the Injector what it should be providing to those constructors.

Unlike Angular 1.x, the Angular 2 Injector doesn't attempt to infer any information about what to inject based on argument names. Instead, we have to be explicit about what exactly it should be injecting. This is where annotations come in. First, we need to import some things from the di library:

import {Inject, annotate} from 'di';

The annotation used for injecting things is, unsurprisingly, called Inject. It works so that we give it the tokens of all the things we want to inject, and they will be resolved to the actual dependencies when the object is instantiated. Both AppView and Filters, need an instance of TodoList. As we've seen, the class name can be used as the token. With this information, we can "annotate" those classes accordingly:

annotate(AppView, new Inject(TodoList));
annotate(Filters, new Inject(TodoList));

But, arguably, the experimental "ES6+" version of the annotations is much nicer (and if you've ever used Guice, should look very familiar):

@Inject(TodoList)
export class AppView extends View {

  constructor(todos) {
    this.todos = todos;
    //...
  }

  // ...

}
@Inject(TodoList)
export class Filters extends Router {

  constructor(todos) {
    this.todos = todos;
    // ...
  }

  // ...
}

If you want to use this style of annotations, make sure traceur.options.experimental is set to true as described above - otherwise they won't work!

Note that it is important that both AppView and Filters get the same TodoList instance as an argument, because the state of the collection needs to be shared. Luckily the DI framework gives us the same singleton instance each time.

A More Difficult Case: Injecting To TodoView

One other global variable we might want to replace with dependency injection is TodoFilter, which is a String variable lying around on the top level of todo-app.js:

var TodoFilter = '';

Let's get rid of it, and make the current value of the filter injectable instead. Now, TodoFilter is a raw String, and we could just inject that into where its needed. But that doesn't quite cut it since JavaScript strings are immutable and we need to be able to change the filter value over time. So let's make TodoFilter a class instead, and make it wrap the current value of the filter:

class TodoFilter {
  constructor() {
    this.value = '';
  }
}

Again, both AppView and Filters need the filter value, and we can just inject it. For AppView:

@Inject(TodoList, TodoFilter)
export class AppView extends View {

  constructor(todos, filter) {
    this.todos = todos;
    this.filter = filter;
    // ...
  }

  // ...

}

When referring to the filter value, we just need to use this.filter.value instead of TodoFilter:

this.$('#filters li a')
  .removeClass('selected')
  .filter('[href="#/' + (this.filter.value || '') + '"]')
  .addClass('selected');

The same thing applies to Filters. Do note that this time we're using todoFilter as the field value instead of filter. That's because otherwise it would clash with the existing filter() method:

@Inject(TodoList, TodoFilter)
export class Filters extends Router {

  constructor(todos, todoFilter) {
    this.todos = todos;
    this.todoFilter = todoFilter;
    this.routes = {
      '*filter': 'filter'
    }

    this._bindRoutes();
  }

  filter(param = '') {
    this.todoFilter.value = param;
    this.todos.trigger('filter');
  }
}

The final location where the filter value is used is more tricky: We need it in the TodoView class. The problem is that this class is instantiated by giving it an options object, which is passed along to the Backbone View constructor. This is a common pattern in Backbone apps:

var view = new TodoView({ model });

This conflicts with di.js, which also wants to use the constructor for its own purposes.

Another problem is that we can't really use the injector to instantiate TodoViews since we want a different, new TodoView each time, not the same one. Since the injector only uses singletons at the moment, we can't use it for this.

While you can create multiple instances of a dependency by creating multiple Injectors (that may inherit from each other), it doesn't really feel like a good solution for this use case. Each todo item would need to have its own injector.

The di.js issue tracker has an issue where both of these problems are discussed, and indeed it seems they're working on it. But for now, we need a workaround.

One thing you can do is create a new class, whose only job is to instantiate TodoViews: A TodoViewFactory. Unlike TodoView, we can use dependency injection with it. It will then pass those dependencies on to each TodoView that's created, along with the Backbone view options:

@Inject(TodoFilter)
class TodoViewFactory {

  constructor(filter) {
    this.filter = filter;
  }

  create(options) {
    return new TodoView(this.filter, options);
  }
}

TodoView can then just take both the filter object and the options object in its constructor. Notably, we do not annotate it, because we're not using the injector to instantiate it:

class TodoView extends View {

  constructor(filter, options) {
    this.filter = filter;
    // ...
  }

  // ...
}

In AppView, where todo views are actually created, we can inject and use TodoViewFactory:

@Inject(TodoList, TodoFilter, TodoViewFactory)
export class AppView extends View {

  constructor(todos, filter, todoViewFactory) {
    this.todos = todos;
    this.filter = filter;
    this.todoViewFactory = todoViewFactory;
    // ...
  }

  // ...

  addOne(model) {
    var view = this.todoViewFactory.create({ model });
    $('#todo-list').append(view.render().el);
  }

  // ...
}

I'm not super-happy with this approach, since the factory is just boilerplate for something I would like the DI framework to be able to do for me. But it isn't too bad, and it does seem like they're addressing this.

Update: There is now a much better solution for the problem of "partially" injecting an object's constructor. We can now annotate the TodoView constructor, even if all of the arguments will not be provided by the DI framework:

@Inject(TodoFilter, 'options')
class TodoView extends View {

  constructor(filter, options) {
    this.filter = filter;
    // ...
  }

  // ...
}

The 'options' argument here is not something that the DI framework knows about. We'll provide it in a moment as we construct the view.

In AppView, we'll use a new annotation, @InjectLazy to inject a provider for the todo view. Let's import it first:

@Import {Inject, InjectLazy} from 'di';

Since there are now two different inject annotations used, we can't just put a single @Inject on the class, and instead need to annotate each of the constructor arguments separately:

export class AppView extends View {

  constructor(
    @Inject(TodoList) todos,
    @Inject(TodoFilter) filter,
    @InjectLazy(TodoView) createTodoView) {
    this.todos = todos;
    this.filter = filter;
    this.createTodoView = createTodoView;
    // ...
  }

  // ...

}

createTodoView is a function provided by the DI framework, that knows how to lazily create a TodoView class. When we use the function to create a new TodoView, we can provide the options object, matching the 'options' string token the TodoView constructor is annotated with:

  addOne(model) {
    var view = this.createTodoView('options', { model });
    $('#todo-list').append(view.render().el);
  }

So, by using @InjectLazy, we can have the DI framework provide some of the dependencies, and provide the rest ourselves as we construct the dependency.

DI For Non-Classes: Template Injection

So far we've been using the injector to inject instances of classes, but we can use it for other things as well.

The templates used by TodoView and AppView are good candidates for injection: Currently we're just grabbing them from the page with jQuery. I can easily imagine not enjoying testing that. What we can do instead is just inject the templates, which would make it easy to inject custom HTML directly from unit tests.

Since the templates are not classes and have no constructors, we need a new approach for making them. We need a Provider. And for making Providers, we need the Provide annotation:

import {Inject, InjectLazy, Provide} from 'di';

A Provider is something that says "when you see this token, let me know and I'll make you the dependency". So far we've seen tokens that are their own providers - classes - but the two can also be separate. This is a provider for the "itemTemplate" string token:

@Provide('itemTemplate')
export function createItemTemplate() {
  return _.template($('#item-template').html());
}

It is a plain JavaScript function that returns a LoDash template

We're also exporting the function here, but that's just because we're going to need it outside the `todo-app.js` module.

When we inject something using the "itemTemplate" token, we get the return value of the provider function:

@Inject(TodoFilter, 'itemTemplate', 'options')
class TodoView {

  constructor(filter, itemTemplate, options) {
    // ...
    this.itemTemplate = itemTemplate;
  }

  // ...
}

No need to touch the DOM in TodoView to obtain the template! If you run this code, however, it does not work yet. It complains about not being able to inject 'itemTemplate'. What gives?

The problem is that the Injector doesn't know about our provider function. While we have annotated it with Provide, that does not magically add it to any injectors. Before, when we had our annotations right in our tokens, the injector could just look them up, but "external" providers need to be explicitly passed to Injectors. In app.js:

import {Injector} from 'di';
import {AppView, Filters, createItemTemplate} from './todo-app';

$(() => {
  var injector = new Injector([createItemTemplate]);
  injector.get(AppView);
  injector.get(Filters);
  Backbone.history.start();
});

Injector Modules

Having the Injector know about a low-level detail such as createItemTemplate is perhaps not totally appropriate. Large applications may have large amounts of providers, and having to pass each one to Injector is not what you want to be doing.

Instead, we can pass whole ES6 modules to the injector. The injector will then look up all exported variables from those modules and register them.

For the template(s), we could have a templates.js module in our application:

import {Provide} from 'di';

@Provide('itemTemplate')
export function createItemTemplate() {
  return _.template($('#item-template').html());
}

@Provide('statsTemplate')
export function createStatsTemplate() {
  return _.template($('#stats-template').html());
}

Then we could import it to a variable in app.js and pass it along to the Injector, which would register all the exported providers within:

import {Injector} from 'di';
import {AppView, Filters, createItemTemplate} from './todo-app';
module templateModule from './templates';

$(() => {
  var injector = new Injector([templateModule]);
  injector.get(AppView);
  injector.get(Filters);
  Backbone.history.start();
});

Conclusion

It's definitely early days yet for di.js, and I would not consider using it in production at this point. The features are limited and there's no comprehensive documentation. But that's all to be expected at this point. What's more important is that the library has a sound design and is pleasant to use, and I do feel it meets those criteria.

If you want to play with the library, feel free to grab my TodoMVC fork and continue from there. I'd also suggest looking at the examples and docs in the di.js repository, as well as the React example Vojta Jina has published.

Know Your AngularJS Inside Out

Build Your Own AngularJS

Build Your Own AngularJS helps you understand everything there is to understand about AngularJS (1.x). By creating your very own implementation of AngularJS piece by piece, you gain deep insight into what makes this framework tick. Say goodbye to fixing problems by trial and error and hello to reasoning your way through them

eBook Available Now

comments powered by Disqus