Using ES6 Classes as Angular 1.x directives

Using ES6 Classes as Angular 1.x directives

I’m doing a small project to play around the goody bag the ES6 is bringing, I’m trying to set register a class as an angular directive, but I’m running into this error “TypeError: Cannot call a class as a function”, but from the examples I’m finding they just write the class and register it with angular as a directive. Here’s my directive.
class dateBlock {
constructor () {
this.template = ‘/app/dateblock/dateblock.html’;
this.restrict = ‘AE’;
this.scope = {};
}
};

export default dateBlock

and my index where I import it and then declare it.
import calendarController from ‘./calendar/calendar.js’
import dateBlock from ‘./dateblock/dateblock.js’

function setup($stateProvider) {
$stateProvider
.state(‘base’, {
url: ”,
controller: calendarController,
templateUrl: ‘/app/calendar/calendar.html’
});
};

setup.$inject = [‘$stateProvider’]

var app = angular.module(‘calApp’,[‘ngAnimate’,’ui.router’,’hmTouchEvents’, ‘templates’])
.config(setup)
.controller(‘calendarController’, calendarController)
.directive(‘dateBlock’, dateBlock)

If I missed some crucial step I’d love to hear it. Also side question is it cleaner to import all the apps components to the index and register them all there or export the app and import and register within the components?

Solutions/Answers:

Solution 1:

From my point of view, there is no need to use external libraries like register.js, because you can create directive as a ES6 class in this way:

class MessagesDirective {
    constructor() {
        this.restrict = 'E'
        this.templateUrl = 'messages.html'
        this.scope = {}
    }

    controller($scope, $state, MessagesService) {
        $scope.state = $state;
        $scope.service = MessagesService;
    }

    link(scope, element, attrs) {
        console.log('state', scope.state)
        console.log('service', scope.service)
    }
}
angular.module('messages').directive('messagesWidget', () => new MessagesDirective)

Using directive controller allows you to inject dependencies, even without additional declaration (ex. MessagesDirective.$inject = ['$scope', '$state', 'MessagesService']), so you can use services in link function via scope if you need.

Solution 2:

As mentioned in a comment, the module.directive() method expects a factory function rather than a constructor.

The most simple way would be to wrap your class in a function that returns an instance:

angular.module('app')
    .directive('dateBlock', () => new DateBlock());

However, this will only work in the most limited sense – it does not allow for dependency injection and the compile and link functions of your directive (if defined) will not work as expected.

Related:  Fake “click” to activate an onclick method

In fact, this is a problem I have looked into quite extensively and it turned out to be fairly tricky to solve (for me at least).

I wrote an extensive article covering my solution, but as far as you are concerned I can point you to the discussion of the two main issues that need to be resolved:

  1. Dynamically converting a class definition into an angular-compatible factory function

  2. Allowing a directive’s link and compile functions to be defined as class methods

The full solution involves too much code to paste here, I think, but I have put together a working demo project which allows you to define a directive as an ES6 class like this:

class MyDirective {
    /*@ngInject*/
    constructor($interval) {
        this.template = '<div>I\'m a directive!</div>';
        this.restrict = 'E';
        this.scope = {}
        // etc. for the usual config options

        // allows us to use the injected dependencies
        // elsewhere in the directive (e.g. compile or link function)
        this.$interval = $interval;
    }

    // optional compile function
    compile(tElement) {
        tElement.css('position', 'absolute');
    }

    // optional link function
    link(scope, element) {
        this.$interval(() => this.move(element), 1000);
    }

    move(element) {
        element.css('left', (Math.random() * 500) + 'px');
        element.css('top', (Math.random() * 500) + 'px');
    }
}

// `register` is a helper method that hides all the complex magic that is needed to make this work.
register('app').directive('myDirective', MyDirective);

Check out the demo repo here and here is the code behind register.directive()

Solution 3:

@Michael is right on the money:

the module.directive() method expects a factory function

However I solved it using another technique, a little cleaner I suppose, It works fine for me, it’s not perfect though…
I defined a static method that returns a the factory expected by module()

class VineDirective {
    constructor($q) {
        this.restrict = 'AE';
        this.$q = $q;
    }

    link(scope, element, attributes) {
        console.log("directive link");
    }

    static directiveFactory($q){
        VineDirective.instance = new VineDirective($q);
        return VineDirective.instance;
    }
}

VineDirective.directiveFactory.$inject = ['$q'];

export { VineDirective }

And in my app I do:

angular.module('vineyard',[]).directive('vineScroller', VineDirective.directiveFactory)

I believe there’s no other way to use classes + directives that going through hacks like this at this point, just pick the easy one 😉

Related:  OO Javascript constructor pattern: neo-classical vs prototypal

Solution 4:

A simpler, cleaner and more readable solution 🚀.

class ClipBoardText {

  constructor() {
    console.log('constructor');

    this.restrict = 'A';
    this.controller = ClipBoardTextController;
  }

  link(scope, element, attr, ctr) {

    console.log('ctr', ctr);
    console.log('ZeroClipboard in link', ctr.ZeroClipboard);
    console.log('q in link', ctr.q);

  }

  static directiveFactory() {
    return new ClipBoardText();
  }
}

// do not $inject like this
// ClipBoardText.$inject = ['$q'];

class ClipBoardTextController {
  constructor(q) {
    this.q = q;
    this.ZeroClipboard = 'zeroclipboard';
  }
}

ClipBoardTextController.$inject = ['$q'];


export default ClipBoardText.directiveFactory;

You cannot get $q in link function, this in link will be undefined or null. exploring-es6-classes-in-angularjs-1-x#_section-factories

when Angular invokes the link function, it is no longer in the context of the class instance, and therefore this.$interval will be undefined

So make use of the controller function in the directive, and inject dependencies or anything that you want to access in the link function.

Solution 5:

My solution:

class myDirective {
   constructor( $timeout, $http ) {
       this.restrict = 'E';
       this.scope = {};

       this.$timeout = $timeout;
       this.$http = $http;
   }
   link() {
       console.log('link myDirective');
   }
   static create() {
       return new myDirective(...arguments);
   }
}

myDirective.create.$inject = ['$timeout', '$http'];

export { myDirective }

and in the main app file

app.directive('myDirective', myDirective.create)

Solution 6:

In my project I use a syntax sugar for injections. And ES6 makes it pretty simple to use injectable factories for directives avoiding too much duplicate code. This code allows injection inheritance, uses annotated injections and so on. Check this:

First step

Declare base class for all angular controllers\directives\services – InjectableClient. Its main task – set all injected params as properties for ‘this’. This behavior can be overridden, see examples below.

class InjectionClient {

    constructor(...injected) {
        /* As we can append injections in descendants we have to process only injections passed directly to current constructor */ 
        var injectLength = this.constructor.$inject.length;
        var injectedLength = injected.length;
        var startIndex = injectLength - injectedLength;
        for (var i = startIndex; i < injectLength; i++) {
            var injectName = this.constructor.$inject[i];
            var inject = injected[i - startIndex];
            this[injectName] = inject;
        }
    }

    static inject(...injected) {
        if (!this.$inject) { 
            this.$inject = injected; 
        } else {
            this.$inject = injected.concat(this.$inject);
        }
    };
}

For example, if we call SomeClassInheritedFromInjectableClient.inject(‘$scope’), in directive or controller we will use it as ‘this.$scope’

Related:  window.open with headers

Second step

Declare the base class for directive with static method “factory()”, which binds $injected property of directive class to factory function. And also “compile()” method, which binds the context of link function to the directive itself. Its allows to use our injected values inside the link function as this.myInjectedService.

class Directive extends InjectionClient {
    compile() {
        return this.link.bind(this);
    }

    static factory() {
        var factoryFunc = (...injected) => {
            return new this(...injected);
        }
        factoryFunc.$inject = this.$inject;
        return factoryFunc;
    }
}

Third step

Now we can declare as much directive classes as possible. With inheritance. And we can set up injections in simple way with spread arrays (just dont forget call super method). See examples:

class DirectiveFirst extends Directive {
}

DirectiveFirst.inject('injA', 'injB', 'injC');


class DirectiveSecond extends DirectiveFirst {

    constructor(injD, ...injected) {
        super(...injected);
        this.otherInjectedProperty = injD;
    }
}
// See appended injection does not hurt the ancestor class
DirectiveSecond.inject('injD');

class DirectiveThird extends DirectiveSecond {

    constructor(...injected) {
        // Do not forget call the super method in overridden constructors
        super(...injected);
    }
}    

The last step

Now register directives with angular in simple way:

angular.directive('directiveFirst', DirectiveFirst.factory());
angular.directive('directiveSecond', DirectiveSecond.factory());
angular.directive('directiveThird', DirectiveThird.factory());

Now test the code:

var factoryFirst = DirectiveFirst.factory();
var factorySec = DirectiveSecond.factory();
var factoryThird = DirectiveThird.factory();


var directive = factoryFirst('A', 'B', 'C');
console.log(directive.constructor.name + ' ' + JSON.stringify(directive));

directive = factorySec('D', 'A', 'B', 'C');
console.log(directive.constructor.name + ' ' + JSON.stringify(directive));

directive = factoryThird('D', 'A', 'B', 'C');
console.log(directive.constructor.name + ' ' + JSON.stringify(directive));

This will return:

DirectiveFirst {"injA":"A","injB":"B","injC":"C"}
DirectiveSecond {"injA":"A","injB":"B","injC":"C","otherInjectedProperty":"D"}
DirectiveThird {"injA":"A","injB":"B","injC":"C","otherInjectedProperty":"D"}

References