【问题标题】:Add ng-click dynamically in directive link function在指令链接功能中动态添加 ng-click
【发布时间】:2014-04-02 17:06:45
【问题描述】:

我正在尝试创建一个指令,该指令允许将元素定义为可点击或不可点击,并且可以这样定义:

<page is-clickable="true">
    transcluded elements...
</page>

我希望生成的 HTML 是:

<page is-clickable="true" ng-click="onHandleClick()">
    transcluded elements...
</page>

我的指令实现如下所示:

app.directive('page', function() {
    return {
        restrict: 'E',
        template: '<div ng-transclude></div>',
        transclude: true,
        link: function(scope, element, attrs) {
            var isClickable = angular.isDefined(attrs.isClickable) && scope.$eval(attrs.isClickable) === true ? true : false;

            if (isClickable) {
                attrs.$set('ngClick', 'onHandleClick()');
            }

            scope.onHandleClick = function() {
                console.log('onHandleClick');
            };
        }
    };
});

我可以看到添加新属性后,Angular 不知道ng-click,所以它没有触发。我尝试在设置属性后添加$compile,但这会导致无限链接/编译循环。

我知道如果isClickable 的值是true,我可以检查onHandleClick() 函数内部,但我很好奇如何通过动态添加ng-click 事件来执行此操作,因为我可能需要与多个其他 ng-* 指令一起完成此操作,我不想增加不必要的开销。有什么想法吗?

【问题讨论】:

  • 我刚开始学习角度,从我所学到的动态添加东西更像是 jQuery 的东西。我认为您应该使用带有静态模板的替代方法。我昨天遇到了同样的情况,如果按钮不可点击,我只是在点击处理程序中执行 if else 并添加一个条件 ng-class 用于样式设置。我同意 angular 的观点,即拥有一个静态模板很容易阅读和预测功能。
  • 是的,我暂时只检查点击处理程序内部的isClickable 值......我只是觉得必须有一种方法可以在指令编译阶段添加它!无论我在编译函数中尝试什么,Angular 都不会绑定到函数。
  • 在下面查看我的更新答案,了解我最终使用的解决方案。

标签: javascript angularjs angularjs-directive


【解决方案1】:

我觉得应该这样更好:

app.directive('page', function() {
    return {
        restrict: 'E',
        template: '<div ng-transclude></div>',
        transclude: true,
        link: function(scope, element, attrs) {
            var isClickable = angular.isDefined(attrs.isClickable) && scope.$eval(attrs.isClickable) === true ? true : false;

            if (isClickable) {
                angular.element(element).on('click', scope.onHandleClick);
            }

            scope.onHandleClick = function() {
                console.log('onHandleClick');
            };
        }
    };
});

【讨论】:

    【解决方案2】:

    这是我的@DiscGolfer 解决方案版本,我还添加了对属性的支持。

    .directive("page", function() {
    
      return {
        transclude: true,
        replace: true,
        template: function(tElement, tAttr) {
    
          var isClickable = angular.isDefined(tAttrs.isClickable) && eval(tAttrs.isClickable) === true ? true : false;
    
          if (isClickable) {
            tElement.attr("ng-click", "onHandleClick()");
          }
          tElement.attr("ng-transclude", "");
          if (tAttr.$attr.page === undefined) {
            return "<" + tElement[0].outerHTML.replace(/(^<\w+|\w+>$)/g, 'div') + ">";
          } else {
            tElement.removeAttr(tAttr.$attr.page);
            return tElement[0].outerHTML;
          }
        }
    
      };
    

    提供了更通用和完整的示例http://plnkr.co/edit/4PcMnpq59ebZr2VrOI07?p=preview

    此解决方案的唯一问题是 replace 在 AngularJS 中已被弃用。

    【讨论】:

      【解决方案3】:
      module.factory("ibDirectiveHelpers", ["ngClickDirective", function (ngClick) {
              return {
                  click: function (scope, element, fn) {
                      var attr = {ngClick: fn};
                      ngClick[0].compile(element, attr)(scope, element, attr);
                  }
              };
          }]);
      

      使用:

      module.controller("demoController",["$scope","$element","ibDirectiveHelpers",function($scope,$element,ibDirectiveHelpers){
      
      $scope.demoMethod=function(){console.log("demoMethod");};
      ibDirectiveHelpers.click($scope,$element,"demoMethod()");//uses html notation
       //or
      ibDirectiveHelpers.click($scope,$element,function(){$scope.demoMethod();});//uses inline notation
      }]
      

      【讨论】:

      • 添加更多关于如何使用的信息。目前这没用。
      • ibDirectiveHelpers.click(scope,element,function(){//})
      【解决方案4】:

      更好的解决方案(新):

      在阅读了Angular docs之后,我发现了这个:

      您可以将模板指定为表示模板的字符串或 接受两个参数 tElement 和 tAttrs 的函数(在 下面的编译函数api)并返回一个字符串值表示 模板。

      所以我的新指令看起来像这样:(我相信这是处理这类事情的适当“Angular”方式)

      app.directive('page', function() {
          return {
              restrict: 'E',
              replace: true,
              template: function(tElement, tAttrs) {
                  var isClickable = angular.isDefined(tAttrs.isClickable) && eval(tAttrs.isClickable) === true ? true : false;
      
                  var clickAttr = isClickable ? 'ng-click="onHandleClick()"' : '';
      
                  return '<div ' + clickAttr + ' ng-transclude></div>';
              },
              transclude: true,
              link: function(scope, element, attrs) {
                  scope.onHandleClick = function() {
                      console.log('onHandleClick');
                  };
              }
          };
      });
      

      注意新的模板功能。现在我在编译之前在该函数中操作模板。

      替代解决方案(旧):

      添加了replace: true 以消除重新编译指令时出现的无限循环问题。然后在链接函数中,我只是在添加新属性后重新编译元素。不过有一点需要注意,因为我的元素上有一个 ng-transclude 指令,所以我需要将其删除,这样它就不会在第二次编译时尝试嵌入任何内容,因为没有什么可以嵌入的。

      这是我的指令现在的样子:

      app.directive('page', function() {
          return {
              restrict: 'E',
              replace: true,
              template: '<div ng-transclude></div>',
              transclude: true,
              link: function(scope, element, attrs) {
                  var isClickable = angular.isDefined(attrs.isClickable) && scope.$eval(attrs.isClickable) === true ? true : false;
      
                  if (isClickable) {
                      attrs.$set('ngClick', 'onHandleClick()');
                      element.removeAttr('ng-transclude');
                      $compile(element)(scope);
                  }
      
                  scope.onHandleClick = function() {
                      console.log('onHandleClick');
                  };
              }
          };
      });
      

      我认为第二次重新编译模板并不理想,所以我觉得在模板第一次编译之前还有办法做到这一点。

      【讨论】:

      • 很好,你发现了一种可能性,但我认为这很混乱。你应该看看我更新的答案。
      • 我同意,我不会使用它,但至少它确实有效。仍在寻找更好的解决方案...
      • 这很酷(您的新解决方案)。所以现在模板是动态生成的。但我有点不喜欢使用 templateUrl 不再是一种选择,以防标记更复杂。
      • 是的,我认为这个解决方案只适用于内联模板......您可能会使用$httpcompile 阶段获取模板,对其进行操作,然后使用我的替代解决方案在链接阶段并再次$compile。只是摆脱绑定听起来很难看,虽然哈哈。
      • 另一件事是这仅适用于元素而不适用于属性指令。
      【解决方案5】:

      您总是可以将您的 ng-click 修改为如下所示:

      ng-click="isClickable &amp;&amp; someFunction()"

      不需要自定义指令:)

      这是一个演示它的 JSFiddle:http://jsfiddle.net/robianmcd/5D4VR/

      【讨论】:

      • 这很漂亮!如何将 isClickable 属性分配给每个按钮?
      • @Hans isClickable 将是作用域上的一个变量,您可以手动将它或任何其他变量添加到 html 中的任何 ng-click。
      • 虽然这可行,但它并不理想,因为ng-click 指令仍被绑定(我正试图摆脱它,并整理绑定)。请参阅我的更新答案,了解解决我的问题的正确方法。
      • 在你的小提琴中,buttonIsClickable 是 myCtrl 范围内的一个变量。那么 buttonIsClickable 不会暴露给 myCtrl 范围内的所有内容吗?假设您制作了另一个按钮,那么您必须在作用域上定义两个类似 buttonIsClickable 的变量?
      • @Hans 是的,如果你想分别控制两个按钮,你会使用两个变量。
      【解决方案6】:

      HTML

      <div page is-clickable="true">hhhh</div>
      

      JS

      app.directive('page', function($compile) {
                      return {
                          priority:1001, // compiles first
                          terminal:true, // prevent lower priority directives to compile after it
                          template: '<div ng-transclude></div>',
                          transclude: true,
                          compile: function(el,attr,transclude) {
                              el.removeAttr('page'); // necessary to avoid infinite compile loop
                              var contents = el.contents().remove();
                              var compiledContents;
                              return function(scope){
                                  var isClickable = angular.isDefined(attr.isClickable)?scope.$eval(attr.isClickable):false;
                                  if(isClickable){
                                      el.attr('ng-click','onHandleClick()');
                                      var fn = $compile(el);
                                      fn(scope);
                                      scope.onHandleClick = function() {
                                          console.log('onHandleClick');
                                      };
                                  }
                                  if(!compiledContents) {
                                      compiledContents = $compile(contents, transclude);
                                  }
                                  compiledContents(scope, function(clone, scope) {
                                      el.append(clone); 
                                  });
      
                              };
                          },
                          link:function(scope){
      
                          }
      
      
                      };
                  });
      

      感谢Erstad.StephenIlan Frumer

      BTW with restrict: 'E' 浏览器崩溃:(

      【讨论】:

      • 好主意,但这仍然有一个$compile 电话(而且有点丑哈哈)在阅读了角度文档后,我想出了一个很好的解决方案。请参阅我的更新答案。
      • 我同意它非常丑陋 :) 但是管理指令语法很好 :P 总而言之我回答了这个问题 ^^
      • 你作弊了:PP
      • 很高兴知道还有另一种替代解决方案,我非常感谢您的回答!
      • 只有赞赏我的分数才不会增加^^
      【解决方案7】:

      更新答案

      “The Angular Way”根本不是手动的 DOM 操作。所以,我们需要摆脱添加和删除属性。

      DEMO

      将模板更改为:

      template: '<div ng-click="onHandleClick()" ng-transclude></div>'
      

      并在指令中检查isClickable 属性以决定单击时要执行的操作:

          link: function(scope, element, attrs) {
              var isClickable = angular.isDefined(attrs.isClickable) && scope.$eval(attrs.isClickable) === true ? true : false;
      
              scope.onHandleClick = function() {
                  if (!isClickable) return;
                  console.log('onHandleClick');
              };
          }
      

      您还可以将 isClickable 属性放在指令范围内,以便它可以动态更改其行为。

      旧答案(错误)

      link 在模板编译后运行。编译前使用controller修改模板:

      app.directive('page', function() {
          return {
              restrict: 'E',
              template: '<div ng-transclude></div>',
              transclude: true,
              controller: function(scope, element, attrs) {
                  // your code
              }
          };
      });
      

      【讨论】:

      • 不幸的是,这对我不起作用,不过是个好主意。我在compile 函数中也试过这个,但没有运气......
      • 你是对的。我创造了一种新方法。让我知道你喜欢它。
      • 是的,这种方法是我的“后备”,我目前正在使用它作为替代方案......我认为必须有一种方法可以在它之前的指令中操作模板是第一次编译,不是吗?
      • 我认为“后备”有一个巨大的优势:没有 DOM 操作。改变模板并不是 Angular 的本意。另请参阅:stackoverflow.com/questions/14994391/…
      • 不要误会我的意思,在任何正常情况下我都会(并且应该)使用这种方法。我的问题是关于不使用这个。我需要一些稍微不同的东西来删除尽可能多的不需要的绑定(我正在开发一个缺乏性能的移动应用程序中的复杂屏幕)。我的例子非常愚蠢。
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2013-01-27
      • 1970-01-01
      • 1970-01-01
      • 2013-04-22
      • 2017-03-23
      • 2015-07-26
      • 2013-05-24
      相关资源
      最近更新 更多