it-swarm.com.de

Fügen Sie Richtlinien aus der Direktive in AngularJS hinzu

Ich versuche, eine Direktive zu erstellen, die sich um fügt, um weitere Direktiven zu dem Element hinzuzufügen, für das sie deklariert ist .. Zum Beispiel möchte ich eine Direktive erstellen, die sich um das Hinzufügen von datepicker, datepicker-language und ng-required="true" kümmert.

Wenn ich versuche, diese Attribute hinzuzufügen und dann $compile zu verwenden, generiere ich offensichtlich eine Endlosschleife. Ich überprüfe also, ob ich die erforderlichen Attribute bereits hinzugefügt habe:

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

Wenn ich das Element nicht $compilee, werden die Attribute natürlich gesetzt, aber die Direktive wird nicht gebootet.

Ist dieser Ansatz richtig oder mache ich ihn falsch? Gibt es einen besseren Weg, um dasselbe Verhalten zu erreichen?

UDPATE: Da $compile der einzige Weg ist, dies zu erreichen, gibt es eine Möglichkeit, den ersten Kompilierungsdurchlauf zu überspringen (das Element kann mehrere Kinder enthalten)? Vielleicht durch Einstellen von terminal:true?

UPDATE 2: Ich habe versucht, die Direktive in ein select-Element zu setzen, und die Kompilierung wird wie erwartet zweimal ausgeführt, was bedeutet, dass die Anzahl der erwarteten options doppelt so groß ist.

195
frapontillo

In Fällen, in denen Sie mehrere Direktiven für ein einzelnes DOM-Element haben und in denen die Reihenfolge, in der sie angewendet werden, von Bedeutung ist, können Sie die Anwendung mit der priority -Eigenschaft anordnen. Höhere Zahlen laufen zuerst. Die Standardpriorität ist 0, wenn Sie keine angeben.

EDIT: Nach der Diskussion ist hier die vollständige funktionierende Lösung. Der Schlüssel war, das Attribut zu entfernen: element.removeAttr("common-things"); und auch element.removeAttr("data-common-things"); (falls Benutzer data-common-things in der HTML angeben)

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

Working Plunker ist verfügbar unter: http://plnkr.co/edit/Q13bUt?p=preview

Oder:

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

Erklärung, warum wir terminal: true und priority: 1000 (eine hohe Zahl) einstellen müssen:

Wenn das DOM bereit ist, geht angular das DOM durch, um alle registrierten Anweisungen zu identifizieren und die Anweisungen nacheinander basierend auf priority zu kompilieren wenn sich diese Anweisungen auf demselben Element befinden . Wir setzen die Priorität unserer benutzerdefinierten Direktive auf eine hohe Zahl, um sicherzustellen, dass sie kompiliert wird first ​​und mit terminal: true werden die anderen Direktiven nach dieser Direktive skipped zusammengestellt.

Wenn unsere benutzerdefinierte Direktive kompiliert wird, ändert sie das Element, indem sie Direktiven hinzufügt und sich selbst entfernt und den Dienst $ compile verwendet, um alle Direktiven zu kompilieren (einschließlich der übersprungenen).

Wenn wir terminal:true und priority: 1000 nicht festlegen, besteht die Möglichkeit, dass einige Anweisungen kompiliert werden vor unserer benutzerdefinierten Anweisung. Und wenn unsere benutzerdefinierte Direktive $ compile verwendet, um das Element zu kompilieren => kompilieren Sie die bereits kompilierten Direktiven erneut. Dies führt zu unvorhersehbarem Verhalten, insbesondere wenn die vor unserer benutzerdefinierten Direktive kompilierten Direktiven das DOM bereits transformiert haben.

Weitere Informationen zu Priorität und Terminal finden Sie unter Wie wird das Terminal der Direktive verstanden?

Ein Beispiel für eine Anweisung, die auch die Vorlage ändert, ist ng-repeat (Priorität = 1000). Wenn ng-repeat kompiliert wird, ng-repeat erstellen Sie zuvor Kopien des Vorlagenelements andere Richtlinien werden angewendet .

Dank @ Izhakis Kommentar ist hier der Verweis auf ngRepeat Quellcode: https://github.com/angular/angular.js/blob/master/src/ng/directive/ngRepeat.js

258
Khanh TO

Sie können all dies mit nur einem einfachen Vorlagen-Tag erledigen. Ein Beispiel dazu finden Sie unter http://jsfiddle.net/m4ve9/ . Beachten Sie, dass ich in der Super-Direktive-Definition eigentlich keine Compile- oder Link-Eigenschaft benötigte.

Während des Kompilierungsvorgangs zieht Angular die Vorlagenwerte vor dem Kompilieren ein, so dass Sie dort weitere Anweisungen hinzufügen können, und Angular kümmert sich darum.

Wenn dies eine Super-Direktive ist, die den ursprünglichen internen Inhalt beibehalten muss, können Sie transclude : true verwenden und das Innere durch <ng-transclude></ng-transclude> ersetzen.

Hoffe, das hilft, lass es mich wissen, wenn etwas unklar ist

Alex

10
mrvdot

Hier ist eine Lösung, die die Anweisungen, die dynamisch hinzugefügt werden müssen, in die Ansicht verschiebt und optional eine (grundlegende) Bedingungslogik hinzufügt. Dies hält die Direktive ohne fest codierte Logik sauber.

Die Direktive akzeptiert ein Array von Objekten. Jedes Objekt enthält den Namen der hinzuzufügenden Direktive und den Wert, der an sie übergeben werden soll (falls vorhanden).

Ich hatte Mühe, an einen Anwendungsfall für eine solche Direktive zu denken, bis ich dachte, dass es nützlich sein könnte, eine Bedingungslogik hinzuzufügen, die nur eine Direktive basierend auf einer Bedingung hinzufügt (obwohl die Antwort unten immer noch erfunden ist). Ich habe eine optionale if -Eigenschaft hinzugefügt, die einen bool-Wert, einen Ausdruck oder eine Funktion enthalten soll (z. B. in Ihrem Controller definiert), die bestimmt, ob die Direktive hinzugefügt werden soll oder nicht.

Ich verwende auch attrs.$attr.dynamicDirectives, um die genaue Attributdeklaration abzurufen, die zum Hinzufügen der Direktive (z. B. data-dynamic-directive, dynamic-directive) verwendet wird, ohne die zu überprüfenden Zeichenfolgenwerte fest zu codieren.

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

Ich wollte meine Lösung hinzufügen, da die akzeptierte für mich nicht ganz funktionierte.

Ich musste eine Direktive hinzufügen, aber meine auf dem Element behalten.

In diesem Beispiel füge ich dem Element eine einfache ng-style-Direktive hinzu. Um unendliche Kompilierungsschleifen zu verhindern und mir zu erlauben, meine Direktive beizubehalten, habe ich vor dem erneuten Kompilieren des Elements eine Überprüfung vorgenommen, um zu sehen, ob das, was ich hinzugefügt habe, vorhanden ist.

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

Speichern Sie den Status in einem Attribut im Element selbst, z. B. superDirectiveStatus="true".

Zum Beispiel:

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

        }

      }
    };
  });

Ich hoffe das hilft dir. 

1
Kemal Dağ

Es gab eine Änderung von 1.3.x zu 1.4.x.

In Angular 1.3.x funktionierte dies:

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

Jetzt in Angular 1.4.x müssen wir Folgendes tun:

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

(Aus der akzeptierten Antwort: https://stackoverflow.com/a/19228302/605586 von Khanh TO).

1
Thomas

Eine einfache Lösung, die in einigen Fällen funktionieren könnte, besteht darin, einen Wrapper zu erstellen, zu kompilieren und dann das ursprüngliche Element an den Wrapper anzuhängen.

So etwas wie...

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

Diese Lösung hat den Vorteil, dass die Dinge einfach gehalten werden, indem das ursprüngliche Element nicht neu kompiliert wird.

Dies würde nicht funktionieren, wenn require der hinzugefügten Direktive eine der Direktiven des ursprünglichen Elements enthält oder wenn das ursprüngliche Element eine absolute Positionierung aufweist.

0
plong0