【问题标题】:Add directives from directive in AngularJS从 AngularJS 中的指令添加指令
【发布时间】:2017-03-23 08:02:21
【问题描述】:

我正在尝试构建一个指令来处理向其声明的元素添加更多指令。 例如,我想构建一个指令来处理添加datepickerdatepicker-languageng-required="true"

如果我尝试添加这些属性然后使用$compile,我显然会生成一个无限循环,所以我正在检查我是否已经添加了所需的属性:

angular.module('app')
  .directive('superDirective', function ($compile, $injector) {
    return {
      restrict: 'A',
      replace: true,
      link: function compile(scope, element, attrs) {
        if (element.attr('datepicker')) { // check
          return;
        }
        element.attr('datepicker', 'someValue');
        element.attr('datepicker-language', 'en');
        // some more
        $compile(element)(scope);
      }
    };
  });

当然,如果我不$compile 元素,属性将被设置但指令不会被引导。

这种方法是正确的还是我做错了?有没有更好的方法来实现相同的行为?

UDPATE:鉴于$compile 是实现这一目标的唯一方法,有没有办法跳过第一个编译过程(元素可能包含多个子元素)?也许通过设置terminal:true

更新 2:我尝试将指令放入 select 元素中,并且正如预期的那样,编译运行了两次,这意味着预期 options 的数量是预期的两倍。

【问题讨论】:

    标签: javascript angularjs model-view-controller mvvm angularjs-directive


    【解决方案1】:

    如果您在单个 DOM 元素上有多个指令并且 它们的应用顺序,您可以使用priority 属性对其进行排序 应用。较大的数字首先运行。如果您不指定优先级,则默认优先级为 0。

    编辑:经过讨论,这是完整的工作解决方案。关键是删除属性element.removeAttr("common-things");,还有element.removeAttr("data-common-things");(如果用户在html中指定data-common-things

    angular.module('app')
      .directive('commonThings', function ($compile) {
        return {
          restrict: 'A',
          replace: false, 
          terminal: true, //this setting is important, see explanation below
          priority: 1000, //this setting is important, see explanation below
          compile: function compile(element, attrs) {
            element.attr('tooltip', '{{dt()}}');
            element.attr('tooltip-placement', 'bottom');
            element.removeAttr("common-things"); //remove the attribute to avoid indefinite loop
            element.removeAttr("data-common-things"); //also remove the same attribute with data- prefix in case users specify data-common-things in the html
    
            return {
              pre: function preLink(scope, iElement, iAttrs, controller) {  },
              post: function postLink(scope, iElement, iAttrs, controller) {  
                $compile(iElement)(scope);
              }
            };
          }
        };
      });
    

    工作插件可在:http://plnkr.co/edit/Q13bUt?p=preview

    或者:

    angular.module('app')
      .directive('commonThings', function ($compile) {
        return {
          restrict: 'A',
          replace: false,
          terminal: true,
          priority: 1000,
          link: function link(scope,element, attrs) {
            element.attr('tooltip', '{{dt()}}');
            element.attr('tooltip-placement', 'bottom');
            element.removeAttr("common-things"); //remove the attribute to avoid indefinite loop
            element.removeAttr("data-common-things"); //also remove the same attribute with data- prefix in case users specify data-common-things in the html
    
            $compile(element)(scope);
          }
        };
      });
    

    DEMO

    解释为什么我们必须设置terminal: truepriority: 1000(一个很大的数字):

    当 DOM 准备好后,Angular 会遍历 DOM 以识别所有已注册的指令,并根据 priority如果这些指令在同一个元素上,将这些指令一一编译。我们将自定义指令的优先级设置为较高的数字,以确保它会被首先编译,而使用terminal: true,其他指令将在编译该指令后跳过

    当我们的自定义指令被编译时,它会通过添加指令和删除自身来修改元素,并使用 $compile 服务来编译所有指令(包括那些被跳过的指令)

    如果我们不设置terminal:truepriority: 1000,则有可能某些指令我们的自定义指令之前编译。当我们的自定义指令使用 $compile 编译元素时 => 再次编译已经编译的指令。这将导致不可预知的行为,特别是如果在我们的自定义指令之前编译的指令已经转换了 DOM。

    有关优先级和终端的更多信息,请查看How to understand the `terminal` of directive?

    同样修改模板的指令示例是 ng-repeat(优先级 = 1000),当编译 ng-repeat 时,ng-repeat 在应用其他指令之前复制模板元素

    感谢@Izhaki的评论,这里引用ngRepeat源码:https://github.com/angular/angular.js/blob/master/src/ng/directive/ngRepeat.js

    【讨论】:

    • 它会向我抛出堆栈溢出异常:RangeError: Maximum call stack size exceeded,因为它会永远编译。
    • @frapontillo:在您的情况下,请尝试添加 element.removeAttr("common-datepicker"); 以避免无限循环。
    • 好的,我已经可以整理出来了,你要设置replace: false,terminal: true,priority: 1000;然后在compile 函数中设置所需的属性并删除我们的指令属性。最后,在compile返回的post函数中,调用$compile(element)(scope)。该元素将在没有自定义指令但添加了属性的情况下定期编译。我试图实现的不是删除自定义指令并在一个过程中处理所有这些:这似乎无法完成。请参考更新的plnkr:plnkr.co/edit/Q13bUt?p=preview.
    • 注意,如果你需要使用编译或链接函数的attributes对象参数,要知道负责插入属性值的指令的优先级为100,你的指令的优先级需要低于这个,否则由于目录是终端,您将只能获得属性的字符串值。见(见this github pull request和这个related issue
    • 作为删除 common-things 属性的替代方法,您可以将 maxPriority 参数传递给编译命令:$compile(element, null, 1000)(scope);
    【解决方案2】:

    您实际上可以通过一个简单的模板标签来处理所有这些。有关示例,请参阅http://jsfiddle.net/m4ve9/。请注意,我实际上不需要超级指令定义上的编译或链接属性。

    在编译过程中,Angular 会在编译前提取模板值,因此您可以在此处附加任何其他指令,Angular 会为您处理。

    如果这是一个超级指令,需要保留原来的内部内容,可以使用transclude : true,将里面替换成<ng-transclude></ng-transclude>

    希望对你有帮助,如果有什么不清楚的地方请告诉我

    亚历克斯

    【讨论】:

    • 谢谢 Alex,这种方法的问题是我无法对标签是什么做出任何假设。在示例中,它是一个日期选择器,即 input 标签,但我想让它适用于任何元素,例如 divs 或 selects。
    • 啊,是的,我错过了。在这种情况下,我建议坚持使用 div 并确保您的其他指令可以处理它。这不是最干净的答案,但最适合 Angular 方法。当引导进程开始编译 HTML 节点时,它已经收集了节点上的所有指令进行编译,因此在此处添加新指令不会被原始引导进程注意到。根据您的需要,您可能会发现将所有内容包装在一个 div 中并在其中工作,这样可以为您提供更大的灵活性,但它也限制了您可以放置​​元素的位置。
    • @frapontillo 您可以将模板用作传入 elementattrs 的函数。我花了很长时间才解决这个问题,但我还没有看到它在任何地方使用过 - 但似乎工作正常:stackoverflow.com/a/20137542/1455709
    【解决方案3】:

    这是一个将需要动态添加的指令移动到视图中并添加一些可选(基本)条件逻辑的解决方案。这使指令保持干净,没有硬编码逻辑。

    指令接受一个对象数组,每个对象包含要添加的指令的名称和传递给它的值(如果有的话)。

    我一直在努力思考这样的指令的用例,直到我认为添加一些仅基于某些条件添加指令的条件逻辑可能很有用(尽管下面的答案仍然是人为的)。我添加了一个可选的if 属性,该属性应包含一个布尔值、表达式或函数(例如,在您的控制器中定义),以确定是否应添加指令。

    我还使用attrs.$attr.dynamicDirectives 来获取用于添加指令的确切属性声明(例如data-dynamic-directivedynamic-directive),而无需检查硬编码字符串值。

    Plunker Demo

    angular.module('plunker', ['ui.bootstrap'])
        .controller('DatepickerDemoCtrl', ['$scope',
            function($scope) {
                $scope.dt = function() {
                    return new Date();
                };
                $scope.selects = [1, 2, 3, 4];
                $scope.el = 2;
    
                // For use with our dynamic-directive
                $scope.selectIsRequired = true;
                $scope.addTooltip = function() {
                    return true;
                };
            }
        ])
        .directive('dynamicDirectives', ['$compile',
            function($compile) {
                
                 var addDirectiveToElement = function(scope, element, dir) {
                    var propName;
                    if (dir.if) {
                        propName = Object.keys(dir)[1];
                        var addDirective = scope.$eval(dir.if);
                        if (addDirective) {
                            element.attr(propName, dir[propName]);
                        }
                    } else { // No condition, just add directive
                        propName = Object.keys(dir)[0];
                        element.attr(propName, dir[propName]);
                    }
                };
                
                var linker = function(scope, element, attrs) {
                    var directives = scope.$eval(attrs.dynamicDirectives);
            
                    if (!directives || !angular.isArray(directives)) {
                        return $compile(element)(scope);
                    }
                   
                    // Add all directives in the array
                    angular.forEach(directives, function(dir){
                        addDirectiveToElement(scope, element, dir);
                    });
                    
                    // Remove attribute used to add this directive
                    element.removeAttr(attrs.$attr.dynamicDirectives);
                    // Compile element to run other directives
                    $compile(element)(scope);
                };
            
                return {
                    priority: 1001, // Run before other directives e.g.  ng-repeat
                    terminal: true, // Stop other directives running
                    link: linker
                };
            }
        ]);
    <!doctype html>
    <html ng-app="plunker">
    
    <head>
        <script src="//code.angularjs.org/1.2.20/angular.js"></script>
        <script src="//angular-ui.github.io/bootstrap/ui-bootstrap-tpls-0.6.0.js"></script>
        <script src="example.js"></script>
        <link href="//netdna.bootstrapcdn.com/twitter-bootstrap/2.3.1/css/bootstrap-combined.min.css" rel="stylesheet">
    </head>
    
    <body>
    
        <div data-ng-controller="DatepickerDemoCtrl">
    
            <select data-ng-options="s for s in selects" data-ng-model="el" 
                data-dynamic-directives="[
                    { 'if' : 'selectIsRequired', 'ng-required' : '{{selectIsRequired}}' },
                    { 'tooltip-placement' : 'bottom' },
                    { 'if' : 'addTooltip()', 'tooltip' : '{{ dt() }}' }
                ]">
                <option value=""></option>
            </select>
    
        </div>
    </body>
    
    </html>

    【讨论】:

    • 用于其他指令模板。它工作得很好,节省了我的时间。非常感谢。
    【解决方案4】:

    我想添加我的解决方案,因为接受的解决方案不太适合我。

    我需要添加一个指令,但也要保留我的元素。

    在本例中,我向元素添加了一个简单的 ng 样式指令。为了防止无限编译循环并允许我保留我的指令,我在重新编译元素之前添加了一个检查以查看我添加的内容是否存在。

    angular.module('some.directive', [])
    .directive('someDirective', ['$compile',function($compile){
        return {
            priority: 1001,
            controller: ['$scope', '$element', '$attrs', '$transclude' ,function($scope, $element, $attrs, $transclude) {
    
                // controller code here
    
            }],
            compile: function(element, attributes){
                var compile = false;
    
                //check to see if the target directive was already added
                if(!element.attr('ng-style')){
                    //add the target directive
                    element.attr('ng-style', "{'width':'200px'}");
                    compile = true;
                }
                return {
                    pre: function preLink(scope, iElement, iAttrs, controller) {  },
                    post: function postLink(scope, iElement, iAttrs, controller) {
                        if(compile){
                            $compile(iElement)(scope);
                        }
                    }
                };
            }
        };
    }]);
    

    【讨论】:

    • 值得注意的是,您不能将其与 transclude 或模板一起使用,因为编译器会尝试在第二轮中重新应用它们。
    【解决方案5】:

    尝试将状态存储在元素本身的属性中,例如superDirectiveStatus="true"

    例如:

    angular.module('app')
      .directive('superDirective', function ($compile, $injector) {
        return {
          restrict: 'A',
          replace: true,
          link: function compile(scope, element, attrs) {
            if (element.attr('datepicker')) { // check
              return;
            }
            var status = element.attr('superDirectiveStatus');
            if( status !== "true" ){
                 element.attr('datepicker', 'someValue');
                 element.attr('datepicker-language', 'en');
                 // some more
                 element.attr('superDirectiveStatus','true');
                 $compile(element)(scope);
    
            }
    
          }
        };
      });
    

    我希望这对你有帮助。

    【讨论】:

    • 谢谢,基本概念保持不变:)。我正在尝试找出一种跳过第一次编译传递的方法。我已经更新了原来的问题。
    • 双重编译以一种糟糕的方式破坏了事物。
    【解决方案6】:

    从 1.3.x 到 1.4.x 发生了变化。

    在 Angular 1.3.x 中这有效:

    var dir: ng.IDirective = {
        restrict: "A",
        require: ["select", "ngModel"],
        compile: compile,
    };
    
    function compile(tElement: ng.IAugmentedJQuery, tAttrs, transclude) {
        tElement.append("<option value=''>--- Kein ---</option>");
    
        return function postLink(scope: DirectiveScope, element: ng.IAugmentedJQuery, attributes: ng.IAttributes) {
            attributes["ngOptions"] = "a.ID as a.Bezeichnung for a in akademischetitel";
            scope.akademischetitel = AkademischerTitel.query();
        }
    }
    

    现在在 Angular 1.4.x 中我们必须这样做:

    var dir: ng.IDirective = {
        restrict: "A",
        compile: compile,
        terminal: true,
        priority: 10,
    };
    
    function compile(tElement: ng.IAugmentedJQuery, tAttrs, transclude) {
        tElement.append("<option value=''>--- Kein ---</option>");
        tElement.removeAttr("tq-akademischer-titel-select");
        tElement.attr("ng-options", "a.ID as a.Bezeichnung for a in akademischetitel");
    
        return function postLink(scope: DirectiveScope, element: ng.IAugmentedJQuery, attributes: ng.IAttributes) {
    
            $compile(element)(scope);
            scope.akademischetitel = AkademischerTitel.query();
        }
    }
    

    (来自接受的答案:来自 Khanh TO 的https://stackoverflow.com/a/19228302/605586)。

    【讨论】:

      【解决方案7】:

      在某些情况下可行的简单解决方案是创建并 $compile 一个包装器,然后将您的原始元素附加到它。

      类似...

      link: function(scope, elem, attr){
          var wrapper = angular.element('<div tooltip></div>');
          elem.before(wrapper);
          $compile(wrapper)(scope);
          wrapper.append(elem);
      }
      

      此解决方案的优点是无需重新编译原始元素,从而使事情变得简单。

      如果添加的任何指令的 require 任何原始元素的指令或原始元素具有绝对定位,这将不起作用。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 2015-06-24
        • 2014-04-07
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多