J'essaie de construire une directive qui prend en charge en ajoutant d'autres directives à l'élément sur lequel elle est déclarée . Par exemple, je veux construire une directive qui prend en charge l'ajout de datepicker
, datepicker-language
et ng-required="true"
.
Si j'essaie d'ajouter ces attributs, puis d'utiliser $compile
, je génère évidemment une boucle infinie. Je vérifie donc si j'ai déjà ajouté les attributs nécessaires:
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);
}
};
});
Bien sûr, si je ne $compile
pas l'élément, les attributs seront définis, mais la directive ne sera pas amorcée.
Est-ce que cette approche est correcte ou est-ce que je le fais mal? Existe-t-il un meilleur moyen d'obtenir le même comportement?
UDPATE: étant donné que $compile
est le seul moyen d'y parvenir, existe-t-il un moyen de passer la première étape de compilation (l'élément peut contenir plusieurs enfants)? Peut-être en définissant terminal:true
?
UPDATE 2 : J'ai essayé de placer la directive dans un élément select
et, comme prévu, la compilation s'exécute deux fois, ce qui signifie qu'il y a deux fois plus de option
s.
Dans les cas où vous avez plusieurs directives sur un seul élément DOM et où l'ordre Dans lequel elles sont appliquées est important, vous pouvez utiliser la propriété priority
pour ordonner leur application Les nombres les plus élevés sont les premiers. La priorité par défaut est 0 si vous n'en spécifiez pas.
EDIT: après la discussion, voici la solution de travail complète. La clé était supprimer l'attribut} _: element.removeAttr("common-things");
, ainsi que element.removeAttr("data-common-things");
(au cas où les utilisateurs spécifient data-common-things
dans le code 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);
}
};
}
};
});
Le programme de travail est disponible sur: http://plnkr.co/edit/Q13bUt?p=preview
Ou:
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);
}
};
});
Explication de la raison pour laquelle nous devons définir terminal: true
et priority: 1000
(un nombre élevé):
Lorsque le DOM est prêt, Angular parcourt le DOM pour identifier toutes les directives enregistrées et les compiler une par une en fonction de priority
si ces directives se trouvent sur le même élément. Nous avons défini la priorité de notre directive personnalisée sur un nombre élevé pour nous assurer qu'elle sera compilée première et avec terminal: true
, les autres directives seront ignorées après la compilation de cette directive.
Lorsque notre directive personnalisée est compilée, il modifiera l'élément en ajoutant des directives, en se supprimant et en utilisant le service $ compile to compilera toutes les directives (y compris celles qui ont été ignorées)}.
Si nous ne définissons pas terminal:true
et priority: 1000
, il se peut que certaines directives soient compilées avant notre directive personnalisée. Et lorsque notre directive personnalisée utilise $ compile pour compiler l'élément =>, recompilez les directives déjà compilées. Cela entraînera un comportement imprévisible, en particulier si les directives compilées avant notre directive personnalisée ont déjà transformé le DOM.
Pour plus d'informations sur la priorité et le terminal, consultez Comment comprendre le `terminal` de la directive?
ng-repeat
(priorité = 1000) est un exemple de directive modifiant également le modèle. Lorsque ng-repeat
est compilé, ng-repeat
crée des copies de l'élément de modèle avant que d'autres directives ne soient appliquées.
Grâce au commentaire de @ Izhaki, voici la référence au code source ngRepeat
: https://github.com/angular/angular.js/blob/master/src/ng/directive/ngRepeat.js
Vous pouvez réellement gérer tout cela avec une simple balise de modèle. Voir http://jsfiddle.net/m4ve9/ pour un exemple. Notez que je n'avais en fait pas besoin d'une propriété de compilation ou de lien dans la définition de super-directive.
Au cours du processus de compilation, Angular extrait les valeurs du modèle avant la compilation afin que vous puissiez y attacher toute directive supplémentaire. Angular s'en occupera pour vous.
S'il s'agit d'une super directive qui doit conserver le contenu interne d'origine, vous pouvez utiliser transclude : true
et remplacer l'intérieur par <ng-transclude></ng-transclude>
.
Espérons que cela aide, laissez-moi savoir si quelque chose n'est pas clair
Alex
Voici une solution qui déplace les directives à ajouter de manière dynamique dans la vue et ajoute également une logique conditionnelle facultative (de base). Ceci garde la directive propre sans logique codée en dur.
La directive prend un tableau d'objets, chaque objet contient le nom de la directive à ajouter et la valeur à lui transmettre (le cas échéant).
J'avais du mal à penser à un cas d'utilisation pour une directive comme celle-ci jusqu'à ce que je pense qu'il pourrait être utile d'ajouter une logique conditionnelle qui ajoute seulement une directive basée sur une condition (bien que la réponse ci-dessous soit toujours artificielle). J'ai ajouté une propriété optionnelle if
qui devrait contenir une valeur booléenne, une expression ou une fonction (définie par exemple dans votre contrôleur) qui détermine si la directive doit être ajoutée ou non.
J'utilise également attrs.$attr.dynamicDirectives
pour obtenir la déclaration d'attribut exacte utilisée pour ajouter la directive (par exemple, data-dynamic-directive
, dynamic-directive
) sans coder en dur les valeurs de chaîne à vérifier.
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>
Je voulais ajouter ma solution car la solution acceptée ne fonctionnait pas vraiment pour moi.
Je devais ajouter une directive, mais aussi garder la mienne sur l'élément.
Dans cet exemple, j'ajoute une directive simple de style ng à l'élément. Pour éviter des boucles de compilation infinies et me permettre de garder ma directive, j'ai ajouté une vérification pour voir si ce que j'avais ajouté était présent avant de recompiler l'élément.
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);
}
}
};
}
};
}]);
Essayez de stocker l'état dans un attribut sur l'élément lui-même, tel que superDirectiveStatus="true"
Par exemple:
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);
}
}
};
});
J'espère que ceci vous aide.
Il y a eu un changement de 1.3.x à 1.4.x.
Dans Angular 1.3.x, cela fonctionnait:
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();
}
}
Maintenant dans Angular 1.4.x, nous devons faire ceci:
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();
}
}
(D'après la réponse acceptée: https://stackoverflow.com/a/19228302/605586 de Khanh TO).
Une solution simple qui pourrait fonctionner dans certains cas consiste à créer et à $ compiler un wrapper, puis à y ajouter votre élément d'origine.
Quelque chose comme...
link: function(scope, elem, attr){
var wrapper = angular.element('<div tooltip></div>');
elem.before(wrapper);
$compile(wrapper)(scope);
wrapper.append(elem);
}
Cette solution a l'avantage de simplifier les choses en ne recompilant pas l'élément d'origine.
Cela ne fonctionnerait pas si aucune des directives require
de la directive ajoutée n'était l'une des directives de l'élément d'origine ou si l'élément d'origine avait un positionnement absolu.