Добавьте директивы из директивы в AngularJS
Я пытаюсь создать директиву, которая позаботится о добавлении дополнительных директив в элемент, для которого она объявлена. Например, я хочу построить директиву, которая заботится о добавлении datepicker
, datepicker-language
а также ng-required="true"
,
Если я попытаюсь добавить эти атрибуты, а затем использовать $compile
Я, очевидно, генерирую бесконечный цикл, поэтому я проверяю, добавил ли я уже необходимые атрибуты:
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);
}
};
});
Конечно, если я не $compile
элемент, атрибуты будут установлены, но директива не будет загружена.
Это правильный подход или я делаю это неправильно? Есть ли лучший способ добиться того же поведения?
UDPATE: учитывая тот факт, что $compile
это единственный способ добиться этого, есть ли способ пропустить первый проход компиляции (элемент может содержать несколько дочерних элементов)? Может быть, установив terminal:true
?
ОБНОВЛЕНИЕ 2: я попытался поместить директиву в select
элемент и, как и ожидалось, компиляция выполняется дважды, что означает, что число ожидаемых вдвое больше option
s.
7 ответов
В тех случаях, когда у вас есть несколько директив для одного элемента DOM, и когда порядок, в котором они применяются, имеет значение, вы можете использовать priority
собственность, чтобы заказать их применение. Более высокие числа запускаются первыми. Приоритет по умолчанию - 0, если вы его не указали.
РЕДАКТИРОВАТЬ: после обсуждения, вот полное рабочее решение. Ключ должен был удалить атрибут: element.removeAttr("common-things");
, а также element.removeAttr("data-common-things");
(если пользователи указывают data-common-things
в 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);
}
};
}
};
});
Работающий плункер доступен по адресу: http://plnkr.co/edit/Q13bUt?p=preview
Или же:
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);
}
};
});
Объяснение, почему мы должны установить terminal: true
а также priority: 1000
(большое число):
Когда DOM готов, Angular обходит DOM, чтобы определить все зарегистрированные директивы и скомпилировать директивы одну за другой на основе priority
если эти директивы находятся на одном элементе. Мы устанавливаем приоритет нашей пользовательской директивы на большое число, чтобы гарантировать, что она будет скомпилирована первой и с terminal: true
другие директивы будут пропущены после компиляции этой директивы.
Когда наша пользовательская директива компилируется, она изменяет элемент, добавляя директивы и удаляя себя, и использует $compile service для компиляции всех директив (включая те, которые были пропущены).
Если мы не установим terminal:true
а также priority: 1000
Есть вероятность, что некоторые директивы скомпилированы до нашей пользовательской директивы. И когда наша пользовательская директива использует $ compile для компиляции element => compile снова уже скомпилированные директивы. Это приведет к непредсказуемому поведению, особенно если директивы, скомпилированные до нашей пользовательской директивы, уже преобразовали DOM.
Для получения дополнительной информации о приоритете и терминале, посмотрите Как понять `терминал` директивы?
Пример директивы, которая также изменяет шаблон: ng-repeat
(приоритет = 1000), когда ng-repeat
компилируется, ng-repeat
сделайте копии элемента шаблона до того, как будут применены другие директивы.
Благодаря комментарию @ Изаки, вот ссылка на ngRepeat
исходный код: https://github.com/angular/angular.js/blob/master/src/ng/directive/ngRepeat.js
Вы можете справиться со всем этим с помощью простого тега шаблона. См. http://jsfiddle.net/m4ve9/ для примера. Обратите внимание, что на самом деле мне не нужно свойство компиляции или ссылки в определении супер-директивы.
В процессе компиляции Angular извлекает значения шаблона перед компиляцией, так что вы можете прикрепить туда любые дополнительные директивы, и Angular позаботится об этом за вас.
Если это супер-директива, которая должна сохранить исходный внутренний контент, вы можете использовать transclude : true
и заменить внутри <ng-transclude></ng-transclude>
Надеюсь, это поможет, дайте мне знать, если что-то неясно
Alex
Вот решение, которое перемещает директивы, которые должны динамически добавляться, в представление, а также добавляет некоторую необязательную (базовую) условную логику. Это поддерживает директиву в чистоте без жестко запрограммированной логики.
Директива принимает массив объектов, каждый объект содержит имя добавляемой директивы и значение для передачи ей (если есть).
Я изо всех сил пытался придумать сценарий использования такой директивы, пока не подумал, что было бы полезно добавить некоторую условную логику, которая добавляет директиву только на основе некоторого условия (хотя ответ ниже все еще надуман). Я добавил опционально if
свойство, которое должно содержать значение bool, выражение или функцию (например, определенную в вашем контроллере), которая определяет, следует ли добавлять директиву или нет.
Я также использую attrs.$attr.dynamicDirectives
чтобы получить точное объявление атрибута, используемое для добавления директивы (например, data-dynamic-directive
, dynamic-directive
) без жестко заданных значений строк для проверки.
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>
Я хотел добавить свое решение, так как принятое решение не совсем для меня.
Мне нужно было добавить директиву, а также сохранить элемент.
В этом примере я добавляю простую директиву в стиле ng к элементу. Чтобы предотвратить бесконечные циклы компиляции и позволить мне сохранить мою директиву, я добавил проверку, чтобы увидеть, присутствовало ли то, что я добавил, перед перекомпиляцией элемента.
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);
}
}
};
}
};
}]);
Try storing the state in a attribute on the element itself, such as superDirectiveStatus="true"
Например:
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);
}
}
};
});
Я надеюсь, это поможет вам.
Произошло изменение с 1.3.x до 1.4.x.
В Angular 1.3.x это сработало:
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();
}
}
Теперь в Angular 1.4.x мы должны сделать это:
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();
}
}
(Из принятого ответа: /questions/27719455/dobavte-direktivyi-iz-direktivyi-v-angularjs/27719481#27719481 от Khanh TO).
Простое решение, которое может работать в некоторых случаях, - это создать и $compile обертку, а затем добавить к ней исходный элемент.
Что-то вроде...
link: function(scope, elem, attr){
var wrapper = angular.element('<div tooltip></div>');
elem.before(wrapper);
$compile(wrapper)(scope);
wrapper.append(elem);
}
Преимущество этого решения в том, что оно упрощает задачу, не перекомпилируя исходный элемент.
Это не будет работать, если какая-либо из добавленных директив require
любые директивы исходного элемента или если исходный элемент имеет абсолютное позиционирование.