Определите директиву AngularJS, используя TypeScript и механизм $inject.

Недавно я начал рефакторинг одного из проектов Angular, над которым я работаю с помощью TypeScript. Использование классов TypeScript для определения контроллеров очень удобно и хорошо работает с минифицированными файлами JavaScript благодаря свойству static $inject Array<string>. И вы получаете довольно чистый код без разделения зависимостей Angular от определения класса:

 module app {
  'use strict';
  export class AppCtrl {
    static $inject: Array < string > = ['$scope'];
    constructor(private $scope) {
      ...
    }
  }

  angular.module('myApp', [])
    .controller('AppCtrl', AppCtrl);
}

Прямо сейчас я ищу решение для обработки аналогичного случая для определения директивы. Я нашел хорошую практику для определения директив как функций:

module directives {

  export function myDirective(toaster): ng.IDirective {
    return {
      restrict: 'A',
      require: ['ngModel'],
      templateUrl: 'myDirective.html',
      replace: true,
      link: (scope: ng.IScope, element: ng.IAugmentedJQuery, attrs: ng.IAttributes, ctrls) => 
        //use of $location service
        ...
      }
    };
  }


  angular.module('directives', [])
    .directive('myDirective', ['toaster', myDirective]);
}

В этом случае я вынужден определить зависимости Angular в определении директивы, что может быть очень подвержено ошибкам, если определение и класс TypeScript находятся в разных файлах. Как лучше всего определить директиву с помощью машинописного текста и механизма $inject, я искал хороший способ реализовать интерфейс TypeScript IDirectiveFactory, но найденные решения меня не удовлетворили.


person Milko Lorinkov    schedule 13.11.2014    source источник


Ответы (9)


Использование классов и наследование от ng.IDirective — это способ работы с TypeScript:

class MyDirective implements ng.IDirective {
    restrict = 'A';
    require = 'ngModel';
    templateUrl = 'myDirective.html';
    replace = true;

    constructor(private $location: ng.ILocationService, private toaster: ToasterService) {
    }

    link = (scope: ng.IScope, element: ng.IAugmentedJQuery, attrs: ng.IAttributes, ctrl: any) => {
        console.log(this.$location);
        console.log(this.toaster);
    }

    static factory(): ng.IDirectiveFactory {
        const directive = ($location: ng.ILocationService, toaster: ToasterService) => new MyDirective($location, toaster);
        directive.$inject = ['$location', 'toaster'];
        return directive;
    }
}

app.directive('mydirective', MyDirective.factory());

Связанный ответ: https://stackoverflow.com/a/29223360/990356

person tanguy_k    schedule 24.03.2015
comment
Превосходная работа! Безусловно, самый чистый подход, который я видел! - person Mobiletainment; 02.10.2015
comment
Хороший обходной путь! Однако, чтобы избежать обертывания инъекции, вы также можете использовать простую инъекцию контроллера, как в ответе, предоставленном @Mobiletainment stackoverflow.com/a/32934956/ 40853 - person mattanja; 05.11.2015
comment
Можем ли мы добиться этого без использования функции ссылки? Я использую Angular 1.4, и, поскольку мы будем защищать наш код от Angular 2.0, а функции ссылки там не поддерживаются, я не хочу писать эту логику с помощью функции ссылки. Поэтому, пожалуйста, дайте мне знать, можно ли получить доступ к элементу без функция связи. - person ATHER; 26.05.2016
comment
Вы можете пропустить шаг directive.$inject = ['$location', 'toaster'];, просто добавив 'ngInject'; в функцию-конструктор. - person kayasky; 16.01.2017

Я предпочитаю указывать controller для директивы и исключительно вставлять туда зависимости.

Имея контроллер и его интерфейс, я строго ввожу 4-й параметр функции связи в интерфейс моего контроллера и с удовольствием использую его оттуда.

Смещение проблемы зависимости от части ссылки к контроллеру директивы позволяет мне использовать TypeScript для контроллера, в то время как я могу сохранить свою функцию определения директивы короткой и простой (в отличие от подхода класса директивы, который требует указания и реализации статического фабричного метода для директивы ):

module app {
"use strict";

interface IMyDirectiveController {
    // specify exposed controller methods and properties here
    getUrl(): string;
}

class MyDirectiveController implements IMyDirectiveController {

    static $inject = ['$location', 'toaster'];
    constructor(private $location: ng.ILocationService, private toaster: ToasterService) {
        // $location and toaster are now properties of the controller
    }

    getUrl(): string {
        return this.$location.url(); // utilize $location to retrieve the URL
    }
}

function myDirective(): ng.IDirective {
    return {
        restrict: 'A',
        require: 'ngModel',
        templateUrl: 'myDirective.html',
        replace: true,

        controller: MyDirectiveController,
        controllerAs: 'vm',

        link: (scope: ng.IScope, element: ng.IAugmentedJQuery, attributes: ng.IAttributes, controller: IMyDirectiveController): void => {
            let url = controller.getUrl();
            element.text('Current URL: ' + url);
        }
    };
}

angular.module('myApp').
    directive('myDirective', myDirective);
}
person Mobiletainment    schedule 04.10.2015
comment
ИМХО, это лучший ответ, и я тоже буду это делать, поскольку это не требует какой-либо специальной обработки, обходных путей и т. д. Просто инъекция контроллера по умолчанию. - person mattanja; 05.11.2015
comment
Должен ли контроллер быть зарегистрирован в модуле? - person Blake Mumford; 29.01.2016
comment
@ Блейк Мамфорд нет. В этом случае контроллер директивы является обычным классом. Единственное, что нужно зарегистрировать в Angular — это саму директиву. - person Mobiletainment; 29.01.2016
comment
кто-нибудь знает, почему мой контроллер имеет неопределенный метод getUrl при использовании его в директиве? Я использовал точный код с одним небольшим изменением: angular.module('mezurioApp').directive('myDirective',[myDirective]); (используйте массив в качестве второго аргумента, иначе он не скомпилируется). - person emp; 05.04.2016
comment
Разве требование: 'ngModel' не заставляет контроллер, переданный в функцию ссылки, быть NgModelController, а не MyDirectiveController, который вы определили? - person RMD; 15.04.2016
comment
если я хочу добавить атрибуты в директиву, скажите здесь scope: { test: '= test' } и используйте этот атрибут в контроллере. как это сделать ?. - person Hrishikesh Sardar; 15.02.2017

В этом случае я вынужден определить угловые зависимости в определении директивы, что может быть очень подвержено ошибкам, если определение и класс машинописного текста находятся в разных файлах.

Решение:

 export function myDirective(toaster): ng.IDirective {
    return {
      restrict: 'A',
      require: ['ngModel'],
      templateUrl: 'myDirective.html',
      replace: true,
      link: (scope: ng.IScope, element: ng.IAugmentedJQuery, attrs: ng.IAttributes, ctrls) => 
        //use of $location service
        ...
      }
    };
  }
  myDirective.$inject = ['toaster']; // THIS LINE
person basarat    schedule 13.11.2014
comment
Спасибо, но это все еще выглядит не очень хорошо. Я предпочитаю иметь один блок, который инкапсулирует всю логику внутри него. - person Milko Lorinkov; 14.11.2014
comment
Это то, что сработало для меня. Использование класса для директивы, как предлагали другие, не сработало, потому что у меня не было доступа к этому внутри функции ссылки. - person Sammi; 07.07.2015

Уже немного поздно для этой вечеринки. Но вот решение, которое я предпочитаю использовать. Я лично считаю, что это чище.

Сначала определите вспомогательный класс, и вы сможете использовать его где угодно. (На самом деле его можно использовать для чего угодно, если вы немного измените вспомогательную функцию. Вы можете использовать его для запуска конфигурации и т. д.)

module Helper{
    "use strict";

    export class DirectiveFactory {
        static GetFactoryFor<T extends ng.IDirective>(classType: Function): ng.IDirectiveFactory {
            var factory = (...args): T => {
                var directive = <any> classType;
                //return new directive(...args); //Typescript 1.6
                return new (directive.bind(directive, ...args));
            }
            factory.$inject = classType.$inject;
            return factory;
        }
    }
}

Вот вам основной модуль

module MainAppModule {
    "use strict";

angular.module("App", ["Dependency"])
       .directive(MyDirective.Name, Helper.DirectiveFactory.GetFactoryFor<MyDirective>(MyDirective));

    //I would put the following part in its own file.
    interface IDirectiveScope extends ng.IScope {
    }

    export class MyDirective implements ng.IDirective {

        public restrict = "A";
        public controllerAs = "vm";
        public bindToController = true;    
        public scope = {
            isoVal: "="
        };

        static Name = "myDirective";
        static $inject = ["dependency"];

        constructor(private dependency:any) { }

        controller = () => {
        };

        link = (scope: IDirectiveScope, iElem: ng.IAugmentedJQuery, iAttrs: ng.IAttributes): void => {

        };
    }
}
person maxisam    schedule 06.08.2015
comment
Это требует компиляции в ES6. новая директива(...аргументы); (альтернативная версия делает то же самое). без es6 он помещает зависимости в первый параметр конструктора в виде массива. Знаете ли вы решение, которое работает для ES5? - person Robert Baker; 14.08.2015
comment
пробовал, не работает var toArray = function(arr) { return Array.isArray(arr) ? обр : [].slice.call(обр); }; вернуть новый (directive.bind(directive, toArray(args))); - person Robert Baker; 14.08.2015
comment
Я уверен, что это работает. У вас должна быть последняя версия Typescript. Typescript перенесет его в ES5. - person maxisam; 14.08.2015
comment
Мне пришлось обновить свой ts до сегодняшнего дня (я был на 20150807). Код Visual Studio по-прежнему отображает ошибку, но работает. //возвратить новую директиву(...args); работает - person Robert Baker; 15.08.2015
comment
странный. У меня был Typescript 1.5.3, хотя версия поставляется с VS2015. Я не пробовал это против кода. В любом случае, рад, что у тебя все получилось. - person maxisam; 15.08.2015

Эта статья в значительной степени охватывает это, и ответ от tanguy_k в значительной степени дословно соответствует примеру, приведенному в статье. У него также есть все мотивы, ПОЧЕМУ вы хотели бы написать класс таким образом. Наследование, проверка типов и другие хорошие вещи...

http://blog.aaronholmes.net/writing-angularjs-directives-as-typescript-classes/

person Louis Duran    schedule 19.05.2015

Вот мое решение:

Директива:

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);
  }

}

Модуль (регистрирует директиву...):

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);

Декоратор (добавляет объект директивы inject и product):

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;
  };
}

Я думаю о перемещении module.directive(...), module.service(...) в файлы классов, например. StoryBoxDirective.ts но еще не принял решение и не провел рефакторинг ;)

Вы можете проверить полный рабочий пример здесь: https://github.com/b091/ts-skeleton

Директива находится здесь: https://github.com/b091/ts-skeleton/blob/master/src/module/story/StoryBoxDirective.ts

person b091    schedule 21.08.2015
comment
лучшее решение OO и TS. Рассматривали ли вы, есть ли у вас альтернатива зависимости $rootScope? например связывание только с введенными объектами области действия из контроллера директив? - person OzBob; 23.11.2015

Этот ответ был в некоторой степени основан на ответе @Mobiletainment. Я включаю его только потому, что пытался сделать его немного более читабельным и понятным для начинающих.

module someModule { 

    function setup() { 
        //usage: <some-directive></some-directive>
        angular.module('someApp').directive("someDirective", someDirective); 
    };
    function someDirective(): ng.IDirective{

        var someDirective = {
            restrict: 'E',
            templateUrl: '/somehtml.html',
            controller: SomeDirectiveController,
            controllerAs: 'vm',
            scope: {},
            link: SomeDirectiveLink,
        };

        return someDirective;
    };
    class SomeDirectiveController{

        static $inject = ['$scope'];

        constructor($scope) {

            var dbugThis = true;
            if(dbugThis){console.log("%ccalled SomeDirectiveController()","color:orange");}
        };
    };
    class SomeDirectiveLink{
        constructor(scope: ng.IScope, element: ng.IAugmentedJQuery, attributes: ng.IAttributes, controller){
            var dbugThis = true;
            if(dbugThis){console.log("%ccalled SomeDirectiveLink()","color:orange");}
        }
    };
    setup();
}
person bbuie    schedule 19.04.2016

Другое решение — создать класс, указать статическое свойство $inject и определить, вызывается ли класс с оператором new. Если нет, вызовите новый оператор и создайте экземпляр класса директивы.

вот пример:

module my {

  export class myDirective {
    public restrict = 'A';
    public require = ['ngModel'];
    public templateUrl = 'myDirective.html';
    public replace = true;
    public static $inject = ['toaster'];
    constructor(toaster) {
      //detect if new operator was used:
      if (!(this instanceof myDirective)) {
        //create new instance of myDirective class:
        return new (myDirective.bind.apply(myDirective, Array.prototype.concat.apply([null], arguments)));
      }
    }
    public link(scope: ng.IScope, element: ng.IAugmentedJQuery, attrs: ng.IAttributes, ctrls:any) {

    }
  }

}
person Szymon Wygnański    schedule 18.11.2014

Все варианты ответов натолкнули меня на мысль, что 2 сущности (ng.IDirective и Controller) слишком много для описания компонента. Поэтому я создал простой прототип оболочки, который позволяет их объединять. Вот суть прототипа https://gist.github.com/b1ff/4621c20e5ea705a0f788.

person Evgeniy    schedule 25.11.2015