Dependency Injection for ES6 Style AngularJS Directives

February 17, 2017

Note! This is about directives written in Angular 1.4+ in ES6. In case you were thinking this was covering something new for Angular 2…

I’ve (and the team I’m on) have been writing our Angular apps with ES6 for awhile now, and one thing that has frustrated me (us) has been dealing with directives written as ES6 classes.

Why?

Not because they don’t work as a class. They do. The problem has been dependency injection.


The Problem

The basic setup is easy enough:

export default class SomeDirective {
    constructor(){ 
        this.restrict = 'A'; 
        this.scope = {}; 
    } 
    link(scope, element, attrs){
        //link code here... 
    } 
}

Easy enough. Back in the index/module file:

import SomeDirective from './some-directive';

angular.module('some.directive.someName', []) .directive('SomeDirective, () => new SomeDirective); Here’s where the weirdness starts. The directive won’t work unless we get kind of explicit in making it NEW. Which, isn’t horrible, but different than if we were writing a .controller('name', controllername). The directive as class answer comes from Todd Motto’s Angular Style Guide.


The Initial Solution

But the solution for dependency injection? Well, there’s a little stack overflow (of course there is) answer that fills that gap. The result goes as follows:

export default class SomeDirective {
    constructor(){ 
        this.restrict = 'A'; 
        this.scope = {}; 
    }

    controller($scope, SomeService){
        $scope.dependency = dependency;
    }

    link(scope, element, attrs){
         //link code here... 
    } 
}

and in the index/module:

import SomeService from './some.service';
import SomeDirective from './some.directive';

angular.module('some.directive.someName', [])
.service('SomeService', SomeService)
.directive('SomeDirective, () => new SomeDirective);

Adding a controller to the directive this way, we can then have regular access to our dependencies! Great! I’ve just turned answers from two other places into a collection here in this blog post and hopefully saved you some time searching for them.

BUT!

Here’s an interesting situation:

What if I have a directive that needs dependency injection AND requires a model from either it’s element or further up from a parent?


Secondary Problem

So say my directive’s constructor now looks like this:

export default class SomeDirective {
    constructor(){ 
        this.restrict = 'A';
        this.require = '^ngModel';
        this.scope = {}; 
    }

    controller($scope, SomeService){
        $scope.dependency = dependency;
    }

    link(scope, element, attrs){
        //link code here... 
    } 
}

And let’s say that the model is an input, and I need all the fun controllery things angular attaches to an input (like $pristine).

  1. I can’t put the required model/controller into my controller. It just doesn’t exist at that point in building the directive.
  2. I can’t put it in the link as controller because I’ve just named controller above, it will supercede the require.
  3. I had hoped I could do something like is the Angular docs (the very last example on the page) where I could pull in multiple controllers as an array via require. I still maintain hope that I could get this to work. I’m not convinced it couldn’t…

Secondary Solution –UPDATED!–

So the controller I create above helps me pass things over to the link function, which solves my dependency injection issue, but as stated, using that controller as a passed arg in the link function destroys the require. But I can put the controller into the constructor and then have require also require the directive (which just asks for it’s controller thankfully):

export default class SomeDirective {
    constructor(){ 
        this.restrict = 'A';
        this.require = ['SomeDirective', '^ngModel'];
        this.scope = {};
        this.controller($scope, SomeService){
            $scope.dependency = dependency;
            return {
                iam: () => {
                    return 'a little teapot!'
                }
            }
        } 
    }

    link(scope, element, attrs, ctrls){
        const $ctrl = ctrls[0];
        const $reqCtrl = ctrls[1];
        scope.dependency.someMethodInDepenency();
    } 
}

If you breakpoint this out in your dev tools, you can see that the controller created in the constructor will only have whatever you return. $Scope will be the only thing to have public access to the injected service, but $ctrl will be able to access iam(). Meanwhile $reqCtrl will contain the other controller we required. This matches up nicely with the example I mentioned earlier from the Angular docs.


Conclusion

I’m hoping this collection of answers will save someone some time hunting them. The last solution helps solve an issue of needing both require and dependency injection while preserving the controllers for both in a way Angular would be expecting if this wasn’t built as a class.