AngularJSのディレクティブからディレクティブを追加する


197

宣言されている要素にさらにディレクティブ追加する処理を行うディレクティブを作成しようとしています。たとえばdatepickerdatepicker-languageとの追加を処理するディレクティブを作成したいとしng-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

UPDATE 2:ディレクティブをselect要素に入れてみましたが、予想どおり、コンパイルが2回実行されoptionます。つまり、予想されるの数が2倍になります。

回答:


260

1つのDOM要素に複数のディレクティブがあり、それらが適用される順序が重要な場合は、priorityプロパティを使用してアプリケーションを順序付けることができます。大きい数が最初に実行されます。指定しない場合、デフォルトの優先度は0です。

編集:議論の後、これが完全な実用的な解決策です。重要なのは、属性削除することでした:(またelement.removeAttr("common-things");element.removeAttr("data-common-things");ユーザーdata-common-thingsがHTMLで指定した場合)

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

デモ

設定terminal: trueしなければならない理由とpriority: 1000(高い数値):

DOMの準備ができると、angularはDOMをpriority 調べて登録されているすべてのディレクティブを識別し、これらのディレクティブが同じ要素にあるかどうかに基づいてディレクティブを1つずつコンパイルします。カスタムディレクティブの優先度を高い値に設定して、最初にコンパイルterminal: trueされるようにします。を使用すると、このディレクティブのコンパイル後に他のディレクティブがスキップされます。

カスタムディレクティブがコンパイルされると、ディレクティブを追加してそれを削除することで要素を変更し、$ compileサービスを使用してすべてのディレクティブ(スキップされたものを含む)コンパイルます

terminal:trueandを設定しない場合priority: 1000、一部のディレクティブがカスタムディレクティブの前にコンパイルされる可能性があります。そして、カスタムディレクティブが$ compileを使用して要素をコンパイルするとき=>コンパイル済みのディレクティブを再度コンパイルします。特に、カスタムディレクティブの前にコンパイルされたディレクティブが既にDOMを変換している場合は、予期しない動作が発生します。

優先順位とターミナルの詳細については、ディレクティブの「ターミナル」を理解する方法を確認してください

テンプレートも変更するディレクティブの例はng-repeat(priority = 1000)ng-repeatです。コンパイル時ng-repeat に、他のディレクティブが適用される前にテンプレート要素のコピーを作成します

@Izhakiのコメントのおかげで、ここにngRepeatソースコードへの参照があります:https : //github.com/angular/angular.js/blob/master/src/ng/directive/ngRepeat.js


5
それは私にスタックオーバーフロー例外を投げRangeError: Maximum call stack size exceededます。
frapontillo 2013年

3
@frapontillo:あなたのケースでは、element.removeAttr("common-datepicker");無限ループを避けるために追加してみてください。
カーンTO

4
[OK]を、私はあなたが設定する必要があり、それを整理することができましたreplace: falseterminal: truepriority: 1000、次に、compile関数に必要な属性を設定し、ディレクティブ属性を削除します。最後に、がpost返す関数compileでを呼び出します$compile(element)(scope)。要素は、カスタムディレクティブなしで定期的にコンパイルされますが、属性が追加されます。私が達成しようとしていたのは、カスタムディレクティブを削除せずに、すべてを1つのプロセスで処理することでした。これは実行できないようです。更新されたplnkr:plnkr.co/edit/Q13bUt?p= previewを参照してください。
frapontillo 2013年

2
コンパイルまたはリンク関数の属性オブジェクトパラメータを使用する必要がある場合は、属性値の補間を行うディレクティブの優先度が100であり、ディレクティブの優先度がこれよりも低い必要があることに注意してください。そうでない場合は、ターミナルが終端になっているため、属性の文字列値。参照(このgithubプルリクエスト関連する問題を参照)
Simen Echholt '05年

2
削除の代替としてcommon-thingsの属性を使用するには、コンパイルコマンドにmaxPriorityパラメータを渡すことができます$compile(element, null, 1000)(scope);
アンドレアス・

10

実際には、単純なテンプレートタグでこれらすべてを処理できます。例については、http://jsfiddle.net/m4ve9/を参照してください。スーパーディレクティブの定義では、コンパイルまたはリンクのプロパティは実際には必要なかったことに注意してください。

コンパイルプロセス中に、Angularはコンパイルする前にテンプレート値を取得するため、そこにさらにディレクティブを追加でき、Angularがそれを処理します。

これが元の内部コンテンツを保持する必要があるスーパーディレクティブである場合は、内部を使用transclude : trueして置き換えることができます。<ng-transclude></ng-transclude>

お役に立てれば幸いです。不明な点がある場合はお知らせください

アレックス


アレックスに感謝します。このアプローチの問題は、タグがどのようになるかを想定できないことです。この例では、日付ピッカー、つまりinputタグでしたが、divsやselects などの任意の要素で機能させるようにしたいと思います。
frapontillo 2013年

1
ああ、ええ、私はそれを逃した。その場合は、divを使用し、他のディレクティブがそれで機能することを確認することをお勧めします。これは最もクリーンな回答ではありませんが、Angular方法論に最適です。ブートストラッププロセスがHTMLノードのコンパイルを開始するまでに、ノードのすべてのディレクティブがコンパイル用に収集されているため、新しいディレクティブを追加しても、元のブートストラッププロセスでは気付かれません。必要に応じて、すべてをdivでラップし、その中で作業すると柔軟性が高まりますが、要素を配置できる場所も制限されます。
mrvdot 2013年

3
@frapontilloあなたはとの関数としてテンプレートを使用することができますelementし、attrs渡された作業そのうちに私の年齢を取って、私はそれがどこにも使用され見ていない-しかし、それは仕事の罰金に思える:。stackoverflow.com/a/20137542/1455709
パトリック

6

これは、動的に追加する必要があるディレクティブをビューに移動し、オプションの(基本)条件ロジックを追加するソリューションです。これにより、ハードコードされたロジックがなくてもディレクティブをクリーンに保つことができます。

ディレクティブはオブジェクトの配列を取り、各オブジェクトには追加するディレクティブの名前とそれに渡す値(ある場合)が含まれます。

ある条件に基づいてディレクティブを追加するだけの条件付きロジックを追加すると便利だと思うまで、私はこのようなディレクティブのユースケースを考えるのに苦労していました(ただし、以下の答えはまだ工夫されています)。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>


他のディレクティブテンプレートで使用されます。それはうまくいき、私の時間を節約します。感謝します。
jcstritt 2014年

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

コンパイラーは2番目のラウンドでそれらを再適用しようとするため、transcludeまたはテンプレートではこれを使用できないことに注意してください。
spikyjt 2016年

1

要素自体の属性に状態を保存してみてください。 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);

        }

      }
    };
  });

これがお役に立てば幸いです。


おかげで、基本的な概念は変わりません:)。最初のコンパイルパスをスキップする方法を見つけようとしています。元の質問を更新しました。
frapontillo 2013年

ダブルコンパイルは物事をひどく壊します。
frapontillo 2013年

1

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


0

場合によっては機能する簡単な解決策は、ラッパーを作成して$ compileし、それに元の要素を追加することです。

何かのようなもの...

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

このソリューションには、元の要素を再コンパイルしないことで物事をシンプルに保つという利点があります。

これは、追加されたディレクティブのrequireいずれかが元の要素のディレクティブのいずれかである場合、または元の要素が絶対配置を持つ場合は機能しません。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.