Подтвердить что ты не робот

AngularJS: Какая наилучшая практика для добавления ngIf в директиву программно?

Я хочу создать директиву, которая проверяет, должен ли элемент присутствовать в dom на основе значения, полученного от службы (например, проверить роль пользователя).

Соответствующая директива выглядит следующим образом:

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

В конце элемент имеет прикрепленный атрибут ng-if, но каким-то образом он не применяется к элементу и он все еще существует в dom. Таким образом, это, очевидно, неправильный подход.

Эта скрипта показывает проблему: http://jsfiddle.net/L37tZ/2/

Кто может объяснить, почему это происходит? Есть ли другой способ аналогичного поведения? Существующие ngIfs следует учитывать.

РЕШЕНИЕ:

Использование: <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;
          }
        }
      });
    }
  };
});

Очень важно добавить приоритет в директиве, иначе другие директивы, прикрепленные к этому элементу, не будут оценены!

4b9b3361

Ответ 1

Первая часть вашего вопроса "почему?" - это то, на что я могу ответить:

Проблема, с которой вы сталкиваетесь, заключается в том, что вы не можете динамически применять директивы к элементам без вызова $compile элемента.

Если вы вызываете $compile(element)(element.scope()) после установки атрибута, вы запускаете переполнение стека, потому что вы компилируете себя, что заставляет вас скомпилировать себя, что заставляет вас скомпилировать себя и т.д.

Вторая часть, "как еще добиться", у меня проблемы. Я попробовал несколько подходов (например, переключение содержимого с вложенным ng-if), но я не могу получить именно то поведение, которое вы ищете.

Я думаю, что следующим шагом может стать изучение кода ng-if и попытка реализовать что-то подобное непосредственно в вашей директиве.

Вот первый проход получения работы. Я ожидаю, что это потребует некоторой очистки и модификации, чтобы заставить его работать, как вы действительно этого хотите.

Ответ 2

Вы можете повторно использовать ngIf в своей собственной директиве следующим образом:

/** @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);
    }
  };
});

а затем используйте его так:

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

и он будет использовать все "функции", которые поставляются с помощью ngIf.

Ответ 3

Ответ Joscha довольно хорош, но на самом деле это не сработает, если вы используете ng-if в дополнение к нему. Я взял код Joscha и добавил несколько строк, чтобы объединить его с существующими директивами ng-if:

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 no ng-if, process normally
                ifEvaluator = function () {
                    return User.isAuthenticated();
                }
            }
            attributes.ngIf = ifEvaluator;
            ngIf.link.apply(ngIf, arguments);
        }
    };
}]);

Итак, если можно делать такие вещи, как:

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

И условный div будет отображаться только в том случае, если вы аутентифицированы && & тестовый ввод не пуст.

Ответ 4

Существует еще один способ решить эту проблему, используя функцию шаблонирования. Для этого необходимо, чтобы функция jquery 1.6+ функционировала должным образом.

Рабочий скрипт кода: 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 предотвращает вложенные элементы. Без replace = true строка, возвращаемая функцией шаблона, помещается внутри существующего html. То есть <a href="#" addCondition="'true'">Hello</a> становится <a href="#" ng-if="'true'"><a href="#" ng-if="'true'">Hello</a></a>

Подробнее см. https://docs.angularjs.org/api/ng/service/ $.

Ответ 5

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

Сочетание высокого приоритета и terminal: true является основой того, как это работает: Флаг терминала сообщает Angular пропустить все директивы более низкого приоритета в одном и том же элементе HTML.

Это нормально, потому что мы хотим изменить элемент, заменив add-condition на ng-if перед вызовом compile, который затем обработает ng-if и любые другие директивы.