使用 ES6 类作为 Angular 1.x 指令

Posted

技术标签:

【中文标题】使用 ES6 类作为 Angular 1.x 指令【英文标题】:Using ES6 Classes as Angular 1.x directives 【发布时间】:2015-04-21 15:07:31 【问题描述】:

我正在做一个小项目来玩 ES6 带来的好东西,我正在尝试将注册一个类设置为一个角度指令,但我遇到了这个错误“TypeError:无法调用一个类作为函数”,但从示例中我发现他们只是编写类并将其注册为 angular 作为指令。这是我的指令。

class dateBlock 
  constructor () 
    this.template = '/app/dateblock/dateblock.html';
    this.restrict = 'AE';
    this.scope = ;
  
;

export default dateBlock

以及我导入它然后声明它的索引。

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)

如果我错过了某个关键步骤,我很乐意听到。另外一个问题是将所有应用程序组件导入索引并在那里注册它们或导出应用程序并在组件中导入和注册是否更干净?

【问题讨论】:

directive 需要一个函数来返回诸如scopetemplate 等事物的映射。我会尝试new dateBlock () 而不仅仅是dateBlock 。我会将dateBlock 重命名为DateBlock :-) 谢谢,我重命名为 DateBlock。如果我创建一个 DateBlock 的新实例,我会得到一个具有模板限制和范围属性的对象,我是否应该将 DateBlock 构造函数注册为指令,并在构造函数中包含 this.link 或 this.controller 属性?跨度> 对于像 Angular 这样大的项目,没有任何使用 ES6 语法编写指令的“好”方法,这不是很神奇吗?我找不到一个看起来不像是过度工程或黑客的单一解决方案。 【参考方案1】:

在我看来,没有必要使用 register.js 之类的外部库,因为您可以通过这种方式将指令创建为 ES6 类:

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)

使用指令控制器允许您注入依赖项,即使没有额外的声明(例如MessagesDirective.$inject = ['$scope', '$state', 'MessagesService']),所以如果需要,您可以通过作用域在链接函数中使用服务。

【讨论】:

没有过度设计和清晰。如果可以的话,我会投票更多。谢谢。 作为记录,如果您使用ng-annotate,请不要忘记将controller注释为ngInjectcontroller($scope, $state, MessagesService) 'ngInject'; ...。这么说是因为我在这上面浪费了 2 个小时,:ashamed:. @andrew 天才,谢谢!我永远也想不通。 如果 is 代码被缩小并且变量名被破坏,这将不起作用。一种可能的解决方法是告诉压缩器不要破坏变量名,但这会导致更大的包 ymmv。【参考方案2】:

正如评论中提到的,module.directive() 方法需要一个工厂函数而不是构造函数。

最简单的方法是将你的类包装在一个返回实例的函数中:

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

但是,这只会在最有限的意义上起作用 - 它不允许依赖注入,并且指令的 compilelink 函数(如果已定义)将无法按预期工作。

事实上,这是一个我已经深入研究过的问题,而且解决起来相当棘手(至少对我而言)。

我写了一篇详尽的文章来介绍我的解决方案,但就您而言,我可以为您指出需要解决的两个主要问题的讨论:

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

    Allowing a directive's link and compile functions to be defined as class methods

我认为完整的解决方案涉及的代码太多,无法在此处粘贴,但我已经整理了一个工作演示项目,它允许您将指令定义为 ES6 类,如下所示:

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

查看demo repo here 和here is the code behind register.directive()

【讨论】:

我在尝试转换返回 $resource 实例的工厂服务时遇到了类似的问题。 ***.com/questions/28992224/… 我会高度避免像这样的“黑客”;这将使 2.0 转换变得艰难。 @amcdnl 确实,这确实有点 hacky。在我的文章中,我不建议将其作为做事的好方法,而是对它如何进行的探索。由于这个问题是关于如何做到这一点,并且作者说这是一个使用 ES6 的小项目,所以我冒险在这种情况下升级路径不会是一个主要问题。对于大型或生产项目,我倾向于同意您的看法。 有没有办法在这个结构中添加一个具有服务依赖的控制器?【参考方案3】:

@Michael 是正确的:

module.directive() 方法需要一个工厂函数

但是我使用另一种技术解决了它,我想它更干净一些,它对我来说很好,虽然它并不完美...... 我定义了一个静态方法,它返回 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 

在我的应用中我会这样做:

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

我相信目前没有其他方法可以使用经过这样的黑客攻击的类 + 指令,只需选择简单的 ;-)

【讨论】:

为什么不直接返回 new VineDirective($q) 呢? static directiveFactory($q) 'ngInject'; return new VineDirective($q); 这也有效。我现在认为这种模式是最可靠的。如果您的指令采用表单或输入控制器,则在链接函数中使用指令的控制器来处理依赖项似乎有问题。【参考方案4】:

更简单、更干净、更易读的解决方案?。

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;

您无法在link 函数中获得$qthislink 中将是undefinednull。 exploring-es6-classes-in-angularjs-1-x#_section-factories

当 Angular 调用链接函数时,它不再在类实例的上下文中,因此 this.$interval 将是未定义的

因此,在指令中使用controller 函数,并在link 函数中注入依赖项或您想要访问的任何内容。

【讨论】:

你需要从'ClipBoardTextController'导入ClipBoardTextController; 这似乎是一种干净的方法,特别是因为在模块上注册指令时不需要做任何花哨的事情。如果控制器是在同一个 ES6 模块中定义的,我也不确定 @geniuscarrier 会得到什么。我在控制器的构造函数上使用了 ng-annotate。我要处理的唯一额外内容是传递上下文,因此我可以在指令类的其他方法中使用可注入对象,但除此之外,我认为这是一个很好的解决方案。谢谢【参考方案5】:

我的解决方案:

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 

在主应用文件中

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

【讨论】:

【参考方案6】:

在我的项目中,我使用语法糖进行注入。 ES6 使得使用可注入工厂来避免过多重复代码的指令变得非常简单。这段代码允许注入继承,使用注解注入等等。检查这个:

第一步

声明所有角度控制器\指令\服务的基类 - InjectableClient。 它的主要任务 - 将所有注入的参数设置为“this”的属性。可以覆盖此行为,请参见下面的示例。

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

例如,如果我们调用 SomeClassInheritedFromInjectableClient.inject('$scope'),在指令或控制器中我们将使用它作为 'this.$scope'

第二步

使用静态方法“factory()”声明指令的基类,该方法将指令类的 $injected 属性绑定到工厂函数。还有“compile()”方法,它将链接函数的上下文绑定到指令本身。它允许在链接函数中使用我们注入的值作为 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;
    

第三步

现在我们可以声明尽可能多的指令类。与继承。我们可以使用扩展数组以简单的方式设置注入(只是不要忘记调用超级方法)。查看示例:

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

最后一步

现在用简单的方式注册指令:

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

现在测试代码:

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

这将返回:

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"

【讨论】:

【参考方案7】:
class ToggleShortcut
constructor($timeout, authService, $compile, $state)

    var initDomEvents = function ($element, $scope) 

        var shortcut_dropdown = $('#shortcut');

        $compile(shortcut_dropdown)($scope);

        $scope.goToShortCutItem = function(state, params)
            var p = params || null;

            if(state === 'app.contacts.view')
                var authProfile = authService.profile;
                if(authProfile)
                    p = 
                        id:authProfile.user_metadata.contact_id
                    ;
                
            

            $state.go(state, p);
            window.setTimeout(shortcut_buttons_hide, 300);
        ;

        $element.on('click', function () 
            if (shortcut_dropdown.is(":visible")) 
                shortcut_buttons_hide();
             else 
                shortcut_buttons_show();
            

        );

        // SHORTCUT buttons goes away if mouse is clicked outside of the area
        $(document).mouseup(function (e) 
            if (shortcut_dropdown && !shortcut_dropdown.is(e.target) && shortcut_dropdown.has(e.target).length === 0) 
                shortcut_buttons_hide();
            
        );

        // SHORTCUT ANIMATE HIDE
        function shortcut_buttons_hide() 
            shortcut_dropdown.animate(
                height: "hide"
            , 300, "easeOutCirc");
            $('body').removeClass('shortcut-on');

        

        // SHORTCUT ANIMATE SHOW
        function shortcut_buttons_show() 
            shortcut_dropdown.animate(
                height: "show"
            , 200, "easeOutCirc");
            $('body').addClass('shortcut-on');
        
    ;

    var link = function($scope, $element)
        $timeout(function()
            initDomEvents($element, $scope);
        );
    ;

    this.restrict = 'EA';
    this.link = link;



toggleShortcut.$inject = ['$timeout', 'authService', '$compile', '$state'];

function toggleShortcut($timeout, authService, $compile, $state)
return new ToggleShortcut($timeout, authService, $compile, $state);


angular.module('app.layout').directive('toggleShortcut', toggleShortcut);

【讨论】:

如果你能解释这个答案会很好。它有 cmets,但它只是一个代码转储。提出问题的用户可能需要更多信息【参考方案8】:

我遇到了类似的问题。但在我的情况下,当我部署到生产环境时它工作并失败了。它失败了,因为生产有最新版本的 6to5。 这可以通过使用npm shrinkwrap 来防止。 根据最新的 ES6 规范,你不能使用这样的类。 https://github.com/babel/babel/issues/700

【讨论】:

【参考方案9】:

我遇到了同样的问题。我第一次尝试通过 ES6 类解决问题,但 $inject 我的依赖项有问题。在我意识到 Angular 有几种编写代码的方式后,我尝试了。我完全使用了John Papa 样式,并且在我的rails 应用程序中使用ES6 得到了这个工作代码:

((angular) => 
 'use strict';

  var Flash = ($timeout) => 
   return 
     restrict: 'E',
     scope: 
       messages: '=messages'
     ,
     template: (() => 
       return "<div class='alert flash- message[0] ' ng-repeat = 'message in messages'>" +
                "<div class= 'close' ng-click = 'closeMessage($index)' data-dismiss = 'alert' > × </div>" +
                "<span class= 'message' > message[1] </ span>" +
              "</ div>";
     ),
     link: (scope) => 
       scope.closeMessage = (index) => 
         scope.messages.splice(index, 1)
       ;

      $timeout(() => 
        scope.messages = []
      , 5000);
    
  
;

Flash.$inject = ['$timeout'];

angular.module('Application').directive('ngFlash', Flash);

)(window.angular);

我知道我可以对更多 ES6 风格的函数和变量进行一些改进。 我希望它有所帮助。

【讨论】:

这个解决方案中的类在哪里?问题是关于在指令中使用 ES6 类,您将指令定义为一个函数,只是无缘无故地使用胖箭头。抱歉,我不喜欢这样。【参考方案10】:

我刚才碰到这个问题,看到了这个话题。尝试了讨论中提供的一些方法,我终于以非常简单的方式解决了这个问题:

export default function archiveTreeDirective() 
    'ngInject';

    return 
        restrict: 'E',
        scope: 
            selectedNodes: "="
        ,
        templateUrl: 'app/components/directives/archiveTree/archiveTree.html',
        controller: ArchiveTreeController,
        controllerAs: 'vm',
        bindToController: true
    ;


class ArchiveTreeController 
    constructor() 
        'ngInject';
        ...
    
    ...

我直接使用函数作为 .directive('directiveName',factory) 参数,并导出它,稍后在模块声明中导入它。但是我在导出时错过了“默认”语句,所以出现了错误。添加“默认”关键字后,一切正常!

我发现这种方法也适用于我的路由配置(也可以通过函数方式)。

============ 希望你能理解我蹩脚的英语:)

【讨论】:

以上是关于使用 ES6 类作为 Angular 1.x 指令的主要内容,如果未能解决你的问题,请参考以下文章

使用 ES6 测试 Angular 1.x 组件不会使用 $componentController 加载绑定

如何使用 es6 模板文字作为 Angular 组件输入

javascript Angular JS(1.x)ES6组件示例

如何重新编译使用“replace: true”的 Angular 1.x 指令

Angular 2 - 基类包含子类模板

在 Angular 1.x 中通信组件或指令的有效方式