web-dev-qa-db-fra.com

Ajouter des directives de directive dans AngularJS

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

195
frapontillo

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

DEMO

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 prioritysi 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-repeatcré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

258
Khanh TO

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

10
mrvdot

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.

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

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);
                    }
                }
            };
        }
    };
}]);
3
Sean256

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. 

1
Kemal Dağ

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

1
Thomas

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.

0
plong0