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;
}
}
});
}
};
});
Очень важно добавить приоритет в директиве, иначе другие директивы, прикрепленные к этому элементу, не будут оценены!
Ответы
Ответ 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
и любые другие директивы.