web-dev-qa-db-fra.com

AngularJS: Quelle est la meilleure pratique pour ajouter ngIf à une directive par programme?

Je veux créer une directive qui vérifie si un élément doit être présent dans le dom en fonction d'une valeur provenant d'un service (par exemple, vérifier un rôle d'utilisateur).

La directive correspondante ressemble à ceci:

angular.module('app', []).directive('addCondition', function($rootScope) {
    return {
        restrict: 'A',
        compile: function (element, attr) {
          var ngIf = attr.ngIf,
              value = $rootScope.$eval(attr.addCondition);

          /**
           * Make sure to combine with existing ngIf!
           * I want to modify the expression to be evalued by ngIf here based on a role 
           * check for example
           */
          if (ngIf) {
            value += ' && ' + ngIf;
          }

          attr.$set('ng-if', value);
        }
    };
});

À la fin de l'élément, l'attribut ng-if est attaché, mais il ne s'applique pas à l'élément et il existe toujours dans le dom. Il s'agit donc manifestement d'une mauvaise approche.

Ce violon montre le problème: http://jsfiddle.net/L37tZ/2/

Qui peut expliquer pourquoi cela se produit? Existe-t-il un autre moyen de parvenir à un comportement similaire? Les ngIfs existants doivent être pris en compte.

SOLUTION:

Utilisation: <div rln-require-roles="['ADMIN', 'USER']">I'm hidden when theses role requirements are not satifisfied!</div>

.directive('rlnRequireRoles', function ($animate, Session) {

  return {
    transclude: 'element',
    priority: 600,
    terminal: true,
    restrict: 'A',
    link: function ($scope, $element, $attr, ctrl, $transclude) {
      var block, childScope, roles;

      $attr.$observe('rlnRequireRoles', function (value) {
        roles = $scope.$eval(value);
        if (Session.hasRoles(roles)) {
          if (!childScope) {
            childScope = $scope.$new();
            $transclude(childScope, function (clone) {
              block = {
                startNode: clone[0],
                endNode: clone[clone.length++] = document.createComment(' end rlnRequireRoles: ' + $attr.rlnRequireRoles + ' ')
              };
              $animate.enter(clone, $element.parent(), $element);
            });
          }
        } else {

          if (childScope) {
            childScope.$destroy();
            childScope = null;
          }

          if (block) {
            $animate.leave(getBlockElements(block));
            block = null;
          }
        }
      });
    }
  };
});

Il est très important d'ajouter la priorité dans la directive, sinon les autres directives attachées à cet élément ne sont pas évaluées!

48
Foobar

La première partie de votre question, "pourquoi?", Est une chose à laquelle je peux répondre:

Le problème que vous rencontrez est que vous ne pouvez pas appliquer dynamiquement des directives à des éléments sans appeler $compile Sur l'élément.

Si vous appelez $compile(element)(element.scope()) après avoir défini l'attribut, vous rencontrez un débordement de pile parce que vous vous compilez, ce qui vous oblige à vous compiler ce qui vous oblige à vous compiler, etc.

La deuxième partie, "comment faire autrement", me pose problème. J'ai essayé quelques approches (comme transclure le contenu avec un ng-if Imbriqué) mais je ne peux pas obtenir exactement le comportement que vous recherchez.

Je pense que la prochaine étape pourrait être d'étudier le code de ng-if et d'essayer d'implémenter quelque chose de similaire directement dans votre directive.

Voici un premier passage pour le faire fonctionner . Cependant, je pense qu'il a besoin d'un nettoyage et de modifications pour le faire fonctionner comme vous le souhaitez vraiment.

8
Brian Genisio

Vous pouvez réutiliser ngIf dans votre propre directive comme ceci:

/** @const */ var NAME = 'yourCustomIf';

yourApp.directive(NAME, function(ngIfDirective) {
  var ngIf = ngIfDirective[0];

  return {
    transclude: ngIf.transclude,
    priority: ngIf.priority,
    terminal: ngIf.terminal,
    restrict: ngIf.restrict,
    link: function($scope, $element, $attr) {
      var value = $attr[NAME];
      var yourCustomValue = $scope.$eval(value);

      $attr.ngIf = function() {
        return yourCustomValue;
      };
      ngIf.link.apply(ngIf, arguments);
    }
  };
});

puis l'utiliser comme ça

<div your-custom-if="true">This is shown</div>

et il utilisera toutes les "fonctionnalités" fournies avec l'utilisation de ngIf.

79
Joscha

La réponse de Joscha est assez bonne, mais en fait cela ne fonctionnera pas si vous utilisez ng-if en plus. J'ai pris le code de Joscha et j'ai juste ajouté quelques lignes pour le combiner avec les directives ng-if existantes:

angular.module('myModule').directive('ifAuthenticated', ['ngIfDirective', 'User', function(ngIfDirective, User) {
    var ngIf = ngIfDirective[0];

    return {
        transclude: ngIf.transclude,
        priority: ngIf.priority - 1,
        terminal: ngIf.terminal,
        restrict: ngIf.restrict,
        link: function(scope, element, attributes) {
            // find the initial ng-if attribute
            var initialNgIf = attributes.ngIf, ifEvaluator;
            // if it exists, evaluates ngIf && ifAuthenticated
            if (initialNgIf) {
                ifEvaluator = function () {
                    return scope.$eval(initialNgIf) && User.isAuthenticated();
                }
            } else { // if there's no ng-if, process normally
                ifEvaluator = function () {
                    return User.isAuthenticated();
                }
            }
            attributes.ngIf = ifEvaluator;
            ngIf.link.apply(ngIf, arguments);
        }
    };
}]);

Donc, si vous pouvez faire des choses comme:

<input type="text" ng-model="test">
<div ng-if="test.length > 0" if-authenticated>Conditional div</div>

Et le div conditionnel ne s'affichera que si vous êtes authentifié && l'entrée de test n'est pas vide.

46
hilnius

Il existe une autre façon de résoudre ce problème, en utilisant une fonction de modèle. Cela nécessite jquery 1.6+ pour fonctionner correctement.

Un violon de travail du code: http://jsfiddle.net/w72P3/6/

return {
    restrict: 'A',
    replace: true,
    template: function (element, attr) {
        var ngIf = attr.ngIf;
        var value = attr.addCondition;
        /**
         * Make sure to combine with existing ngIf!
         */
        if (ngIf) {
            value += ' && ' + ngIf;
        }
        var inner = element.get(0);
        //we have to clear all the values because angular
        //is going to merge the attrs collection 
        //back into the element after this function finishes
        angular.forEach(inner.attributes, function(attr, key){
            attr.value = '';
        });
        attr.$set('ng-if', value);
        return inner.outerHTML;            
    }
}

replace: true empêche les éléments intégrés. Sans replace = true, la chaîne renvoyée par la fonction de modèle est placée dans le code HTML existant. C'est à dire. <a href="#" addCondition="'true'">Hello</a> devient <a href="#" ng-if="'true'"><a href="#" ng-if="'true'">Hello</a></a>

Voir https://docs.angularjs.org/api/ng/service/ $ compiler pour plus de détails.

4
brocksamson
return {
    restrict: 'A',
    terminal: true,
    priority: 50000, // high priority to compile this before directives of lower prio
    compile: function compile(element, attrs) {
        element.removeAttr("add-condition"); // avoid indefinite loop
        element.removeAttr("data-add-condition");

        return {
            pre: function preLink(scope, iElement, iAttrs, controller) {  },
            post: function postLink(scope, iElement, iAttrs, controller) { 
                iElement[0].setAttribute('ng-if', iAttrs.addCondition);
                $compile(iElement)(scope);
            }
        };
    }

La combinaison de haute priorité et terminal: true est la base de la façon dont cela fonctionne: l'indicateur de terminal indique Angular pour ignorer toutes les directives de priorité inférieure sur le même élément HTML.

C'est très bien car nous voulons modifier l'élément en remplaçant add-condition avec ng-if avant d'appeler compile, qui traitera alors ng-if et toute autre directive.

1
Markus Pscheidt