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?


A 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…

The 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.

File this under: seems obvious but I didn’t think about it till now.

let event = {
    keyCode: 9,
    preventDefault: jasmine.createSpy('preventDefault')
};

Some context: I’m currently dealing with Karma telling me that a function within an $event doesn’t have preventDefault() as a function. Okay. Well then…

In my head, the $event is just an object. Nothing special. Why would I possibly have a function in it?

Considering my usual use of Spies is to pass through or return some fake data from a service, it never crossed my mind that I’d use it like this.

For further context: this snippet sits just inside the describe() for the test and before any of the it()‘s for the test.

I wish this were a post with things that someone might find useful. Instead it is more a spilling of my exploration with Angular 2 router that they’ve ported to newer versions of Angular 1 (1.5+ in my case).

In a word: Awesome.

In another word: Frustrating.

Why?

Well, it does what it does very well. That is to say it routes and deals with sub routing very handily. Unfortunately it seems to not deal with updating the url with query params in a manner that you (the user) can see easily that will then persist. There are options though: using Instructions, using location.go()…

But they really one seem to work well when navigating to a new route or refreshing the route you’re already on… This is bad in my particular case because I want to keep some none-essential information for the use and for our apis in the url…

There also seems to be some odd separation between $location and the new router’s Location. I can get location.go() to update the url params as a change happens (while $location.search() is also being used in code I haven’t touched yet) then use the router to navigate to a new view. The changes from location.go() don’t carry over, but ones I can’t see but did set with $location.search() persist…

I also noticed I got the best results when I put my query string changes in the router lifecycle hooks (though $routerOnActivate would make our api calls happen x2 to x4 times….).

I have the feeling if I was just using Angular 2 a lot of these issues would solve themselves. IE: I wouldn’t be mixing things from 1 that I use frequently that won’t exist the same way in 2.

The exploration also gives me a good idea the hurdles the team might have to deal with given how we rely on the query string.

Any other controller would be easy to $scope.$watch a changing value. ES6 means that said watch has to be in the constructor. 1

Okay, easy enough to pick up on. BUT:

constructor($dependancies, $scope){
    this.$dependancies = $dependancies;
    $scope.$watch('thingToWatch', (value) => {
        //do stuff
    }
}

looks fine, but thingToWatch should be part of this. Unfortunately the $watch won’t see it. this no longer exists for this, but the controller name does. In the context of how We/I am writing the controller constructor, it’s controllerAs: 'vm' in the directive.

The $watch then becomes:

constructor($dependancies, $scope){
    this.$dependancies = $dependancies;
    $scope.$watch('vm.thingToWatch', (value) => {
        //do stuff
    }
}


  1. The context for this is that we as a team are currently writing directives and their controllers as separate files and our directives largely don’t have a link function.