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 $compile
e, 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 option
s doppelt so groß ist.
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);
}
};
});
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
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
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.
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>
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);
}
}
};
}
};
}]);
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.
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).
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.