web-dev-qa-db-ja.com

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

宣言されている要素により多くのディレクティブを追加するを考慮したディレクティブを作成しようとしています。たとえば、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を設定することによって?

PDATE 2:私はディレクティブをselect要素に入れようとしましたが、予想通り、コンパイルは2回実行されます。つまり、予想されるoptionの数は2倍になります。

195
frapontillo

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

EDIT:議論の後、ここに完全な実用的なソリューションがあります。キーは、属性を削除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: trueおよびpriority: 1000(高い数値)を設定する必要がある理由:

DOMの準備ができたら、angularはDOMを歩いて、登録されているすべてのディレクティブを識別し、priorityに基づいてディレクティブを1つずつコンパイルしますこれらのディレクティブが同じ要素にある場合 。カスタムディレクティブの優先度を高い数値に設定して、コンパイルされるようにしますfirstterminal: trueを使用すると、このディレクティブの後に他のディレクティブはskippedになります。コンパイル済み。

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

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

優先度と端末の詳細については、 ディレクティブの「端末」を理解する方法?

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

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

258
Khanh TO

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

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

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

それが助けになることを願って、何かが明確でないかどうか私に知らせて

アレックス

10
mrvdot

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

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

私はこのようなディレクティブのユースケースを考えるのに苦労していました(ある条件に基づいてディレクティブを追加するだけの条件付きロジックを追加するのが役に立つかもしれないと思うまで(下の答えはまだ考えられませんが))。ディレクティブを追加するかどうかを決定するブール値、式、または関数(たとえば、コントローラで定義されている)を含む必要がある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>
6
GFoley83

受け入れられたものは私のためには全くうまくいきませんでしたので私は私のソリューションを追加したいと思いました。

ディレクティブを追加する必要がありましたが、その要素についても注意を払います。

この例では、要素に単純なng-styleディレクティブを追加しています。無限のコンパイルループを防ぎ、ディレクティブを維持できるようにするために、要素を再コンパイルする前に、追加したものが存在するかどうかを確認するチェックを追加しました。

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

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

        }

      }
    };
  });

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

1
Kemal Dağ

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

(受け入れられた答えから: https://stackoverflow.com/a/19228302/605586 Khanh TOから)。

1
Thomas

場合によってはうまくいく可能性がある簡単な解決策は、ラッパーを作成して$ compileしてから、それに元の要素を追加することです。

何かのようなもの...

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

この解決策には、元の要素を再コンパイルしないことで物事を単純に保つという利点があります。

これは、追加されたディレクティブのrequireのいずれかがオリジナルのエレメントのディレクティブのいずれかである場合、またはオリジナルのエレメントが絶対位置にある場合には機能しません。

0
plong0