Pregunta

So after taking a look at some of the examples of angularjs directives in typescript, it seems most people agree to use functions instead of classes when implementing them.

I would prefer to have them as a class and attempted to implement them as follows:

module directives
{    
    export class search implements ng.IDirective
    {        
        public restrict: string;
        public templateUrl: string;

        constructor()
        {            
            this.restrict = 'AE';
            this.templateUrl = 'directives/search.html';
        }

        public link($scope: ng.IScope, element: JQuery, attributes: ng.IAttributes)
        {
            element.text("Hello world");

        }
    }
} 

Now this works fine. However, I need to have an isolated scope with some attributes and I'm struggling to find out how to include that in the class itself.

logic dictates that since I can have

public restrict: string;
public templateUrl: string;

I should be able to have something like:

public scope;

But I'm not sure if this is correct or how to carry on from there (i.e how to add the attributes to the scope).

Anybody know how to solve this? (hopefully, without having to revert to a function if possible)

Thanks,

¿Fue útil?

Solución 2

Assuming that what you have works without an islolated scope, the following should work with an isolated scope:

module directives
{    
    export class search implements ng.IDirective
    {        
        public restrict = 'AE';
        public templateUrl = 'directives/search.html';
        public scope = {
            foo:'=',
            bar:'@',
            bas:'&'
        };


        public link($scope: ng.IScope, element: JQuery, attributes: ng.IAttributes)
        {
            element.text("Hello world");
        }
    }
}

Otros consejos

Creating directives as classes can be problematic since you still need to involve a factory function to wrap its instantiation. For example:

export class SomeDirective implements ng.IDirective {
    public link = () => {
    }

    constructor() {}
}

What Doesn't Work

myModule.directive('someDirective', SomeDirective);

Since directives are not invoked using 'new' but are just called as factory functions. This will cause problems on what your constructor function actually returns.

What Does (with Caveats)

myModule.directive(() => new SomeDirective());

This works fine provided you don't have any IoC involved, but once you start introducing injectables, you have to maintain duplicate parameter lists for your factory function and your directive contstructor.

export class SomeDirective implements ng.IDirective {
    ...
    constructor(someService: any) {}
}

myModule.directive('someDirective', ['someService', (someService) => new SomeDirective(someService)]);

Still an option if that is what you prefer, but is important to understand how the directive registration is actually consumed.

An alternative approach

The thing that is actually expected by angular is a directive factory function, so something like:

export var SomeDirectiveFactory = (someService: any): ng.IDirective => {
   return {
     link: () => {...}
   };
}
SomeDirectiveFactory.$inject = ['someService']; //including $inject annotations

myModule.directive('someDirective', SomeDirectiveFactory);

This has the benefit of allowing the use of $inject annotations since angular needs it to be on the factory function in this case.

You could always return an instance of your class from the factory function as well:

export var SomeDirectiveFactory = (someService: any): ng.IDirective => {
   return new SomeDirective(someService);
}
SomeDirectiveFactory.$inject = ['someService']; //including $inject annotations

But really depends on your use case, how much duplication of parameter lists you are okay with, etc.

Here is my proposal:

Directive:

import {directive} from '../../decorators/directive';

@directive('$location', '$rootScope')
export class StoryBoxDirective implements ng.IDirective {

  public templateUrl:string = 'src/module/story/view/story-box.html';
  public restrict:string = 'EA';
  public scope:Object = {
    story: '='
  };

  public link:Function = (scope:ng.IScope, element:ng.IAugmentedJQuery, attrs:ng.IAttributes):void => {
    // console.info(scope, element, attrs, this.$location);
    scope.$watch('test', () => {
      return null;
    });
  };

  constructor(private $location:ng.ILocationService, private $rootScope:ng.IScope) {
    // console.log('Dependency injection', $location, $rootScope);
  }

}

Module (registers directive...):

import {App} from '../../App';
import {StoryBoxDirective} from './../story/StoryBoxDirective';
import {StoryService} from './../story/StoryService';

const module:ng.IModule = App.module('app.story', []);

module.service('storyService', StoryService);
module.directive('storyBox', <any>StoryBoxDirective);

Decorator (adds inject and produce directive object):

export function directive(...values:string[]):any {
  return (target:Function) => {
    const directive:Function = (...args:any[]):Object => {
      return ((classConstructor:Function, args:any[], ctor:any):Object => {
        ctor.prototype = classConstructor.prototype;
        const child:Object = new ctor;
        const result:Object = classConstructor.apply(child, args);
        return typeof result === 'object' ? result : child;
      })(target, args, () => {
        return null;
      });
    };
    directive.$inject = values;
    return directive;
  };
}

I thinking about moving module.directive(...), module.service(...) to classes files e.g. StoryBoxDirective.ts but didn't make decision and refactor yet ;)

You can check full working example here: https://github.com/b091/ts-skeleton

Directive is here: https://github.com/b091/ts-skeleton/blob/master/src/module/story/StoryBoxDirective.ts

Here finally i got working a directive as class plus inheritance. In derived directive I extend the scope plus define the templateUrl. You can override any methods from base directive . The key was to return from constructor the instance of directive.Angularjs calls constructor without new keyword. In this case this is of type window I wrapped few lines to check the instance type of this and in case of window I create a new instance of directive. (See Activator class from sample below)

module Realty.directives {
  export class BaseElementWithLabel implements ng.IDirective {
    public restrict = 'E';
    public replace = true;

    public scope = {
      label: '@',
      model: '=',
      icon: '@',
      readonlyElement: '=',
      remark: '@'
    }

    constructor(extendedScope) {
      if (!(this instanceof Window)) {
        extendedScope = extendedScope || {};
        this.scope = angular.extend(this.scope, extendedScope);
      }
    }

    link(scope: ng.IScope, element: ng.IAugmentedJQuery, attributes: ng.IAttributes, controller, transclude) {
      scope['vm'] = {};
    }
  }
}

module Realty.directives {
  export class textareaWithLabel extends Realty.directives.BaseElementWithLabel implements ng.IDirective {
    templateUrl = 'directives/element-form/textarea-with-label-template.html';

    constructor() {
      super({
        rows: '@'
      });
      return Realty.Activator.Activate(this, textareaWithLabel, arguments);
    }
  };
};

module Realty {
  export class Activator {
    public static Activate(instance: any, type, arguments) {
      if (instance instanceof type) {
        return instance;
      }

      return new(type.bind.apply(type, Array.prototype.concat.apply([null], arguments)));
    }
  }
}

Licenciado bajo: CC-BY-SA con atribución
No afiliado a StackOverflow
scroll top