Fügen Sie Anweisungen aus der Anweisung in AngularJS hinzu

197

Ich versuche, eine Direktive zu erstellen, die dafür sorgt , dass dem Element, für das sie deklariert ist, weitere Direktiven hinzugefügt werden . Zum Beispiel möchte ich eine Richtlinie erstellen , die Pflege der Zugabe dauert datepicker, datepicker-languageund ng-required="true".

Wenn ich versuche, diese Attribute hinzuzufügen und dann zu verwenden, $compilegeneriere ich offensichtlich eine Endlosschleife. Daher überprüfe ich, 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 $compiledas Element nicht benutze, 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 das gleiche Verhalten zu erreichen?

UDPATE : Gibt es angesichts der Tatsache, dass dies $compileder einzige Weg ist, dies zu erreichen, eine Möglichkeit, den ersten Kompilierungsdurchlauf zu überspringen (das Element kann mehrere untergeordnete Elemente enthalten)? Vielleicht durch Einstellung terminal:true?

UPDATE 2 : Ich habe versucht, die Direktive in ein selectElement einzufügen, und die Kompilierung wird erwartungsgemäß zweimal ausgeführt, was bedeutet, dass doppelt so viele options erwartet werden .

Frapontillo
quelle

Antworten:

260

In Fällen, in denen Sie mehrere Anweisungen für ein einzelnes DOM-Element haben und die Reihenfolge, in der sie angewendet werden, von Bedeutung ist, können Sie die priorityEigenschaft verwenden, um ihre Anwendung zu ordnen. Höhere Zahlen laufen zuerst. Die Standardpriorität ist 0, wenn Sie keine angeben.

EDIT : Nach der Diskussion ist hier die vollständige Arbeitslösung. Der Schlüssel bestand darin , das Attribut zu entfernen : element.removeAttr("common-things");, und auch element.removeAttr("data-common-things");(falls Benutzer data-common-thingsim 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);
          }
        };
      }
    };
  });

Der funktionierende 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 setzen müssen terminal: trueund priority: 1000(eine hohe Zahl):

Wenn das DOM bereit ist, geht Angular durch das DOM, um alle registrierten Anweisungen zu identifizieren und die Anweisungen einzeln zu kompilieren, basierend darauf, priority ob sich diese Anweisungen auf demselben Element befinden . Wir setzen unsere Priorität der benutzerdefinierten Richtlinie auf eine hohe Zahl , um sicherzustellen , dass es kompiliert werden erste und terminal: truewerden die anderen Richtlinien werden übersprungen , nachdem diese Richtlinie kompiliert wird.

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

Wenn wir das nicht tun gesetzt terminal:trueund priority: 1000, gibt es eine Chance , dass einige Richtlinien zusammengestellt werden , bevor unsere eigene Richtlinie. 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 Anweisung kompilierten Anweisungen das DOM bereits transformiert haben.

Weitere Informationen zu Priorität und Terminal finden Sie unter Wie verstehe ich das Terminal der Richtlinie?

Ein Beispiel für eine Direktive, die auch die Vorlage ändert, ist ng-repeat(Priorität = 1000). Wenn diese ng-repeatkompiliert wird, ng-repeat erstellen Sie Kopien des Vorlagenelements, bevor andere Direktiven angewendet werden .

Dank des Kommentars von @ Izhaki ist hier der Verweis auf den ngRepeatQuellcode: https://github.com/angular/angular.js/blob/master/src/ng/directive/ngRepeat.js

Khanh TO
quelle
5
Es löst für mich eine Stapelüberlauf-Ausnahme aus: RangeError: Maximum call stack size exceededEs wird für immer kompiliert.
Frapontillo
3
@frapontillo: Versuchen Sie in Ihrem Fall das Hinzufügen element.removeAttr("common-datepicker");, um eine unbestimmte Schleife zu vermeiden.
Khanh bis
4
Ok, ich habe in der Lage gewesen , das Problem zu lösen, müssen Sie Satz replace: false, terminal: true, priority: 1000; Stellen Sie dann die gewünschten Attribute in der compileFunktion ein und entfernen Sie unser Direktivenattribut. Rufen Sie schließlich in der von zurückgegebenen postFunktion auf . Das Element wird regelmäßig ohne die benutzerdefinierte Direktive, jedoch mit den hinzugefügten Attributen kompiliert. Was ich erreichen wollte, war, die benutzerdefinierte Direktive nicht zu entfernen und all dies in einem Prozess zu erledigen: Dies kann anscheinend nicht durchgeführt werden. Weitere Informationen finden Sie im aktualisierten plnkr: plnkr.co/edit/Q13bUt?p=preview . compile$compile(element)(scope)
Frapontillo
2
Beachten Sie, dass Sie, wenn Sie den Attributobjektparameter der Kompilierungs- oder Verknüpfungsfunktionen verwenden müssen, wissen müssen, dass die Direktive, die für die Interpolation von Attributwerten verantwortlich ist, die Priorität 100 hat und Ihre Direktive eine niedrigere Priorität als diese haben muss, sonst erhalten Sie nur die Zeichenfolgenwerte der Attribute, da das Verzeichnis ein Terminal ist. Siehe (siehe diese Github Pull-Anfrage und dieses verwandte Problem )
Simen Echholt
2
Als Alternative zum Entfernen der common-thingsAttribute können Sie einen maxPriority-Parameter an den Kompilierungsbefehl übergeben:$compile(element, null, 1000)(scope);
Andreas
10

All dies können Sie mit nur einem einfachen Vorlagen-Tag erledigen. Ein Beispiel finden Sie unter http://jsfiddle.net/m4ve9/ . Beachten Sie, dass ich für die Definition der Super-Direktive keine Kompilierungs- oder Verknüpfungseigenschaft benötigte.

Während des Kompilierungsprozesses zieht Angular vor dem Kompilieren die Vorlagenwerte ein, sodass Sie dort weitere Anweisungen anhängen können, und Angular kümmert sich für Sie darum.

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

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

Alex

mrvdot
quelle
Vielen Dank, Alex. Das Problem bei diesem Ansatz ist, dass ich nicht davon ausgehen kann, wie das Tag aussehen wird. In dem Beispiel war es ein Datepicker, dh ein inputTag, aber ich möchte, dass es für jedes Element wie divs oder selects funktioniert .
Frapontillo
1
Ah ja, das habe ich verpasst. In diesem Fall würde ich empfehlen, bei einem div zu bleiben und nur sicherzustellen, dass Ihre anderen Anweisungen daran arbeiten können. Es ist nicht die sauberste Antwort, passt aber am besten in die Angular-Methodik. Zu dem Zeitpunkt, an dem der Bootstrap-Prozess mit dem Kompilieren eines HTML-Knotens begonnen hat, wurden bereits alle Anweisungen auf dem Knoten zur Kompilierung gesammelt, sodass das Hinzufügen einer neuen Anweisung dort vom ursprünglichen Bootstrap-Prozess nicht bemerkt wird. Abhängig von Ihren Anforderungen finden Sie möglicherweise, dass Sie alles in ein Div einwickeln und darin arbeiten, was Ihnen mehr Flexibilität gibt, aber es schränkt auch ein, wo Sie Ihr Element platzieren können.
Mrvdot
3
@frapontillo Sie können eine Vorlage als Funktion verwenden elementund attrsübergeben. Ich habe ewig
Patrick
6

Hier ist eine Lösung, die die Direktiven, die dynamisch hinzugefügt werden müssen, in die Ansicht verschiebt und außerdem eine optionale (grundlegende) bedingte Logik hinzufügt. Dies hält die Direktive ohne fest codierte Logik sauber.

Die Direktive enthält 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, mir einen Anwendungsfall für eine solche Direktive vorzustellen, bis ich dachte, dass es nützlich sein könnte, eine bedingte Logik hinzuzufügen, die nur eine Direktive hinzufügt, die auf einer bestimmten Bedingung basiert (obwohl die folgende Antwort noch erfunden ist). Ich habe eine optionale ifEigenschaft 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.dynamicDirectivesdie genaue Attribut - Deklaration erhalten verwendet , um die Richtlinie hinzuzufügen (zB data-dynamic-directive, dynamic-directive) ohne Werte hart codierte Zeichenfolge zu überprüfen.

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>

GFoley83
quelle
Wird in einer anderen Direktivenvorlage verwendet. Es funktioniert gut und spart mir Zeit. Nur danke.
JCTRITT
4

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

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

In diesem Beispiel füge ich dem Element eine einfache Direktive im ng-Stil hinzu. Um unendliche Kompilierungsschleifen zu vermeiden und meine Direktive beizubehalten, habe ich vor dem erneuten Kompilieren des Elements überprüft, ob das, was ich hinzugefügt habe, vorhanden war.

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);
                    }
                }
            };
        }
    };
}]);
Sean256
quelle
Es ist erwähnenswert, dass Sie dies nicht mit transclude oder einer Vorlage verwenden können, da der Compiler versucht, sie in der zweiten Runde erneut anzuwenden.
Spikyjt
1

Versuchen Sie, den Status in einem Attribut für das Element selbst zu speichern, z superDirectiveStatus="true"

Beispielsweise:

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.

Kemal Dağ
quelle
Danke, das Grundkonzept bleibt gleich :). Ich versuche einen Weg zu finden, um den ersten Kompilierungsdurchlauf zu überspringen. Ich habe die ursprüngliche Frage aktualisiert.
Frapontillo
Die doppelte Zusammenstellung bricht die Dinge auf schreckliche Weise.
Frapontillo
1

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

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

Thomas
quelle
0

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

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 sie die Dinge einfach hält, indem das ursprüngliche Element nicht neu kompiliert wird.

Dies würde nicht funktionieren, wenn eine der hinzugefügten Anweisungen requireeine der Anweisungen des ursprünglichen Elements ist oder wenn das ursprüngliche Element eine absolute Positionierung hat.

plong0
quelle