Ajouter les directives de la directive AngularJS

j'essaie de construire une directive qui s'occupe de ajoutant plus de directives à l'élément sur lequel elle est déclarée. Par exemple, je veux construire une directive qui prend soin d'ajouter datepicker , datepicker-language et ng-required="true" .

Si j'essaie d'ajouter des attributs et ensuite utiliser $compile j'ai évidemment générer une boucle infinie, donc je vérifie si j'ai déjà ajouté les attributs requis:

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

cette approche Est-elle correcte ou suis-je tout faux? Est-il un meilleur moyen d'atteindre le même comportement?

UDPATE : étant donné que $compile est le seul moyen d'y parvenir, y a-t-il un moyen de sauter la première passe de compilation (l'élément peut contenir plusieurs enfants)? Peut-être par terminal:true ?

UPDATE 2 : j'ai essayé de mettre 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 le nombre prévu de option .

194
demandé sur frapontillo 2013-10-07 15:44:42

7 réponses

dans les cas où vous avez plusieurs directives sur un seul élément DOM et où l'ordre dans lequel ils sont appliqués, vous pouvez utiliser la propriété priority pour commander leur application. Les nombres plus élevés courent en premier. La priorité par défaut est 0 si vous n'en spécifiez pas une.

EDIT : après la discussion, voici la solution de travail complète. L'essentiel était de supprimer l'attribut : element.removeAttr("common-things"); , et aussi element.removeAttr("data-common-things"); (dans le cas où les utilisateurs spécifient data-common-things dans le 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);
          }
        };
      }
    };
  });

plongeur de travail est disponible à: 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);
      }
    };
  });

DÉMO

explication pourquoi nous devons mettre terminal: true et priority: 1000 (un nombre élevé):

quand le DOM est prêt, angulaire marche le DOM pour identifier toutes les directives enregistrées et compiler les directives Un par un basé sur priority si ces directives sont sur le même élément . Nous avons placé la priorité de Notre directive personnalisée à un nombre élevé pour s'assurer qu'elle sera compilée en premier et avec terminal: true , les autres directives seront sautées après cette directive est compilée.

quand notre coutume la directive est compilée, il va modifier l'élément en ajoutant des directives et en se retirant et utiliser $compile service pour compiler toutes les directives (y compris celles qui ont été sautées) .

si nous ne définissons pas terminal:true et priority: 1000 , il y a une chance que certaines directives soient compilées avant Notre directive personnalisée. Et quand notre directive personnalisée utilise $compile pour compiler l'élément = > compile à nouveau le directives compilées. Cela provoquera des comportements imprévisibles surtout si les directives compilées avant notre directive custom ont déjà transformé le DOM.

pour plus d'informations sur la priorité et le terminal, consultez comment comprendre le "terminal" de la directive?

un exemple de directive qui modifie également le modèle est ng-repeat (priorité = 1000), lorsque ng-repeat est compilé, ng-repeat faire des copies de l'élément de modèle avant que d'autres directives soient appliquées .

grâce au commentaire de @Izhaki, voici la référence à ngRepeat code source: https://github.com/angular/angular.js/blob/master/src/ng/directive/ngRepeat.js

256
répondu Khanh TO 2017-05-23 10:31:16

vous pouvez réellement gérer tout cela avec juste un template tag simple. Voir http://jsfiddle.net/m4ve9/ pour un exemple. Notez que je n'avais pas besoin d'une propriété de compilation ou de lien sur la définition de super-directive.

pendant le processus de compilation, Angular tire dans les valeurs de template avant de compiler, de sorte que vous pouvez y joindre d'autres directives et Angular s'en chargera pour vous.

S'il s'agit d'un super directive qui doit préserver le contenu interne d'origine, vous pouvez utiliser transclude : true et remplacer l'intérieur par <ng-transclude></ng-transclude>

Espère que ça aide, laissez-moi savoir si quelque chose n'est pas claire

Alex

10
répondu mrvdot 2013-10-09 15:33:07

Voici une solution qui déplace les directives qui doivent être ajoutées dynamiquement, dans la vue et ajoute également quelques options (de base) conditional-logic. Cela maintient la directive propre sans aucune logique codée.

La directive prend un tableau d'objets, chaque objet contient le nom de la directive, l'ajout de la valeur à transmettre (le cas échéant).

j'avais du mal à trouver une solution pour une directive comme celle-ci jusqu'à ce que je pense que il pourrait être utile d'ajouter de la logique conditionnelle qui n'ajoute qu'une directive basée sur une condition (si la réponse ci-dessous est toujours fictive). J'ai ajouté une propriété optionnelle if qui devrait contenir une valeur bool, une expression ou une fonction (par exemple définie dans votre controller) qui détermine si la directive doit être ajoutée ou non.

j'utilise aussi 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 valeurs de chaîne de caractères à vérifier.

"Demo Plunker

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
répondu GFoley83 2015-04-29 16:16:10

j'ai voulu ajouter ma solution puisque celle acceptée ne fonctionnait pas tout à fait pour moi.

je devais ajouter une directive mais aussi garder la mienne sur l'élément.

dans cet exemple, j'ajoute une simple directive de style ng à l'élément. Pour éviter les boucles de compilation infinies et me permettre de conserver 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);
                    }
                }
            };
        }
    };
}]);
3
répondu Sean256 2018-07-18 14:57:35

Essayez de stocker l'état dans un attribut de l'élément lui-même, comme 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 cela vous aide.

1
répondu Kemal Dağ 2013-10-07 11:51:37

il y a eu un changement de 1.3.x à 1.4.x.

En Angle 1.3.x cela a fonctionné:

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

(de la réponse acceptée: https://stackoverflow.com/a/19228302/605586 from Khanh TO).

0
répondu Thomas 2017-05-23 12:10:30

une solution simple qui pourrait fonctionner dans certains cas est de créer et de compiler un wrapper et d'y ajouter votre élément original.

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 garder les choses simples en ne recompilant pas l'élément d'origine.

cela ne fonctionnerait pas si l'une des directives require de la directive ajoutée était une des directives de l'élément d'origine ou si l'élément d'origine avait positionnement absolu.

0
répondu plong0 2016-02-26 20:31:28